Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.
This commit is contained in:
16
.env.example
Normal file
16
.env.example
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Email Configuration for Nodemailer (Mailtrap SMTP)
|
||||||
|
# For Mailtrap: Use live.smtp.mailtrap.io for production sending
|
||||||
|
# Ports: 587 (recommended), 465, 2525, or 25
|
||||||
|
# Auth: PLAIN or LOGIN
|
||||||
|
# TLS: Required (STARTTLS on 587, 2525, 25; Forced TLS on 465)
|
||||||
|
SMTP_HOST=live.smtp.mailtrap.io
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_SECURE=false
|
||||||
|
SMTP_USER=apismtp@mailtrap.io
|
||||||
|
SMTP_PASS=your-mailtrap-api-token
|
||||||
|
SMTP_FROM=noreply@yourcompany.com
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DATABASE_URL="your-database-url"
|
||||||
|
|
||||||
|
# Other configuration...
|
||||||
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Uploads directory
|
||||||
|
public/
|
||||||
|
public/uploads/
|
||||||
|
|
||||||
|
assets/img/
|
||||||
|
|
||||||
|
|
||||||
0
.nuxtignore
Normal file
0
.nuxtignore
Normal file
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"vue3snippets.enable-compile-vue-file-on-did-save-code": true
|
||||||
|
}
|
||||||
161
NOTIFICATION_TEMPLATES_DEBUG.md
Normal file
161
NOTIFICATION_TEMPLATES_DEBUG.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# Notification Templates Error Debugging Guide
|
||||||
|
|
||||||
|
## Issue Description
|
||||||
|
|
||||||
|
**Error:** `Cannot read properties of undefined (reading 'map')`
|
||||||
|
**Location:** `pages/notification/templates/index.vue` line 399
|
||||||
|
**Cause:** The frontend is trying to call `.map()` on `response.data.templates` but this property is undefined.
|
||||||
|
|
||||||
|
## Root Cause Analysis
|
||||||
|
|
||||||
|
The error occurs because the API response doesn't have the expected structure. The frontend expects:
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
templates: [...], // Array of templates
|
||||||
|
pagination: {...}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
But the API might be returning:
|
||||||
|
- An error response
|
||||||
|
- A response with missing `data` property
|
||||||
|
- A response with missing `data.templates` property
|
||||||
|
- An empty or malformed response
|
||||||
|
|
||||||
|
## Potential Causes
|
||||||
|
|
||||||
|
1. **Database Issues:**
|
||||||
|
- Database connection failure
|
||||||
|
- Missing `notification_templates` table
|
||||||
|
- Table exists but has no data
|
||||||
|
|
||||||
|
2. **Authentication Issues:**
|
||||||
|
- User not authenticated
|
||||||
|
- Authentication middleware failing
|
||||||
|
- Missing user context in API
|
||||||
|
|
||||||
|
3. **API Implementation Issues:**
|
||||||
|
- Error in API endpoint logic
|
||||||
|
- Prisma query failure
|
||||||
|
- Unhandled exceptions
|
||||||
|
|
||||||
|
## Debugging Steps
|
||||||
|
|
||||||
|
### Step 1: Test Database Connection and Data
|
||||||
|
|
||||||
|
Run the database test script:
|
||||||
|
```bash
|
||||||
|
npm run test-api
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Test database connection
|
||||||
|
- Check if `notification_templates` table exists
|
||||||
|
- Show current data count
|
||||||
|
- Simulate the API logic
|
||||||
|
- Display the expected response structure
|
||||||
|
|
||||||
|
### Step 2: Add Sample Data
|
||||||
|
|
||||||
|
If the table is empty, seed it with sample data:
|
||||||
|
```bash
|
||||||
|
npm run seed-templates
|
||||||
|
```
|
||||||
|
|
||||||
|
This will add 4 sample notification templates for testing.
|
||||||
|
|
||||||
|
### Step 3: Run Both Tests
|
||||||
|
|
||||||
|
Run the combined debugging command:
|
||||||
|
```bash
|
||||||
|
npm run debug-templates
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs both the test and seeding scripts.
|
||||||
|
|
||||||
|
### Step 4: Check Authentication
|
||||||
|
|
||||||
|
Ensure you are logged in when testing the frontend. The API requires authentication.
|
||||||
|
|
||||||
|
## Frontend Fixes Applied
|
||||||
|
|
||||||
|
The frontend code has been updated with better error handling:
|
||||||
|
|
||||||
|
1. **Defensive Programming:** Check if response structure exists before accessing
|
||||||
|
2. **Better Error Messages:** More specific error messages based on response type
|
||||||
|
3. **Empty State Handling:** Properly handle empty arrays
|
||||||
|
4. **Loading State Management:** Consistent loading state management
|
||||||
|
|
||||||
|
## API Response Structure
|
||||||
|
|
||||||
|
The API should return:
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
templates: [
|
||||||
|
{
|
||||||
|
id: "uuid",
|
||||||
|
title: "Template Name",
|
||||||
|
value: "template_value",
|
||||||
|
description: "Description",
|
||||||
|
subject: "Email Subject",
|
||||||
|
category: "category",
|
||||||
|
channels: ["email", "push"],
|
||||||
|
status: "Active",
|
||||||
|
version: "1.0",
|
||||||
|
// ... other fields
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
totalCount: 5,
|
||||||
|
totalPages: 1,
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPrevPage: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Fix Summary
|
||||||
|
|
||||||
|
1. **Frontend:** Added robust error handling to prevent crashes
|
||||||
|
2. **Testing:** Created scripts to test database and API logic
|
||||||
|
3. **Sample Data:** Created seeding script for testing
|
||||||
|
4. **Debugging:** Added comprehensive logging and error reporting
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Run `npm run debug-templates` to diagnose the issue
|
||||||
|
2. Check the console output for specific errors
|
||||||
|
3. If database connection fails, check your `DATABASE_URL` environment variable
|
||||||
|
4. If authentication fails, ensure you're logged in
|
||||||
|
5. If the API works in testing but fails in the browser, check browser console for network errors
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `pages/notification/templates/index.vue` - Added error handling
|
||||||
|
- `scripts/seed-templates.js` - Sample data seeding
|
||||||
|
- `scripts/test-api.js` - Database and API testing
|
||||||
|
- `package.json` - Added debugging scripts
|
||||||
|
|
||||||
|
## Environment Requirements
|
||||||
|
|
||||||
|
- Database server running
|
||||||
|
- Valid `DATABASE_URL` in environment
|
||||||
|
- Prisma client generated (`npx prisma generate`)
|
||||||
|
- Database migrations applied
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If the issue persists after following these steps, check:
|
||||||
|
1. Database server status
|
||||||
|
2. Environment variables
|
||||||
|
3. Prisma schema sync with database
|
||||||
|
4. Network connectivity
|
||||||
|
5. Authentication middleware configuration
|
||||||
1
QUEUE_SCHEDULER_IMPLEMENTATION.md
Normal file
1
QUEUE_SCHEDULER_IMPLEMENTATION.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
45
README.md
Normal file
45
README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Nuxt 3 Minimal Starter
|
||||||
|
|
||||||
|
Look at the [nuxt 3 documentation](https://v3.nuxtjs.org) to learn more.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Make sure to install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm install --shamefully-hoist
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Server
|
||||||
|
|
||||||
|
Start the development server on http://localhost:3000
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
Build the application for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Locally preview production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information.
|
||||||
|
# corradAF
|
||||||
|
|
||||||
|
This is the base project for corradAF.
|
||||||
9
app.config.js
Normal file
9
app.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// app.config.ts
|
||||||
|
export default defineAppConfig({
|
||||||
|
nuxtIcon: {
|
||||||
|
size: "24px", // default <Icon> size applied
|
||||||
|
aliases: {
|
||||||
|
nuxt: "logos:nuxt-icon",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
50
app.vue
Normal file
50
app.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup>
|
||||||
|
const { siteSettings, loadSiteSettings } = useSiteSettings();
|
||||||
|
|
||||||
|
// Use site settings for global meta
|
||||||
|
useHead({
|
||||||
|
title: () => siteSettings?.value?.siteName || 'corradAF',
|
||||||
|
meta: [
|
||||||
|
{ name: 'description', content: () => siteSettings?.value?.siteDescription || 'corradAF Base Project' },
|
||||||
|
{ property: 'og:title', content: () => siteSettings?.value?.siteName || 'corradAF' },
|
||||||
|
{ property: 'og:description', content: () => siteSettings?.value?.siteDescription || 'corradAF Base Project' },
|
||||||
|
{ name: 'twitter:title', content: () => siteSettings?.value?.siteName || 'corradAF' },
|
||||||
|
{ name: 'twitter:description', content: () => siteSettings?.value?.siteDescription || 'corradAF Base Project' },
|
||||||
|
],
|
||||||
|
link: [
|
||||||
|
{ rel: 'icon', href: () => siteSettings?.value?.siteFavicon || '/favicon.ico' },
|
||||||
|
{ rel: 'apple-touch-icon', href: () => siteSettings?.value?.siteFavicon || '/favicon.ico' }
|
||||||
|
],
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: "en",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const nuxtApp = useNuxtApp();
|
||||||
|
const loading = ref(true);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// Load site settings first
|
||||||
|
await loadSiteSettings();
|
||||||
|
|
||||||
|
// Hide loading indicator if not hydrating
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Get theme from localStorage or site settings
|
||||||
|
let theme = localStorage.getItem("theme") || siteSettings?.value?.selectedTheme || "biasa";
|
||||||
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VitePwaManifest />
|
||||||
|
<NuxtLoadingIndicator />
|
||||||
|
<NuxtLayout>
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
<NuxtPage :key="$route.fullPath" v-else />
|
||||||
|
</NuxtLayout>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
119
assets/css/menu-levels.css
Normal file
119
assets/css/menu-levels.css
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/* Multi-level menu styling */
|
||||||
|
.multi-level-menu {
|
||||||
|
border-left: 2px solid rgba(var(--color-primary), 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Common styles for all menu levels */
|
||||||
|
.navigation-item-wrapper a {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Long menu text handling */
|
||||||
|
.navigation-item-wrapper span {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show full text on hover */
|
||||||
|
.navigation-item-wrapper a:hover span {
|
||||||
|
white-space: normal;
|
||||||
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced tooltip effect for very long menu items */
|
||||||
|
.deepest-menu-item a:hover span {
|
||||||
|
background-color: rgba(var(--sidebar-menu), 0.95);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Level-specific styles */
|
||||||
|
.second-level-menu {
|
||||||
|
font-weight: 500;
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.second-level-menu .mx-3,
|
||||||
|
.second-level-menu .mx-4 {
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.third-level-menu {
|
||||||
|
font-style: italic;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.third-level-menu .mx-3,
|
||||||
|
.third-level-menu .mx-4 {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deepest-menu-item {
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
border-left: 2px solid rgba(var(--color-primary), 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deepest-menu-item .mx-3,
|
||||||
|
.deepest-menu-item .mx-4 {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
max-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add visual indicators for each level */
|
||||||
|
.second-level-menu a::before,
|
||||||
|
.third-level-menu a::before,
|
||||||
|
.deepest-menu-item a::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
margin-right: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.second-level-menu a::before {
|
||||||
|
background-color: rgba(var(--color-primary), 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.third-level-menu a::before {
|
||||||
|
background-color: rgba(var(--color-accent), 0.8);
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deepest-menu-item a::before {
|
||||||
|
background-color: rgba(var(--color-secondary), 0.8);
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resonsive adjustments for different screen sizes */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.second-level-menu .mx-3,
|
||||||
|
.second-level-menu .mx-4,
|
||||||
|
.third-level-menu .mx-3,
|
||||||
|
.third-level-menu .mx-4,
|
||||||
|
.deepest-menu-item .mx-3,
|
||||||
|
.deepest-menu-item .mx-4 {
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.navigation-item-wrapper span {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
assets/js/formkit-custom.js
Normal file
20
assets/js/formkit-custom.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { createInput } from "@formkit/vue";
|
||||||
|
import OneTimePassword from "~/components/formkit/OneTimePassword.vue";
|
||||||
|
import MaskText from "~/components/formkit/TextMask.vue";
|
||||||
|
import FileDropzone from "~/components/formkit/FileDropzone.vue";
|
||||||
|
import Toggle from "~/components/formkit/Toggle.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
otp: createInput(OneTimePassword, {
|
||||||
|
props: ["digits"],
|
||||||
|
}),
|
||||||
|
mask: createInput(MaskText, {
|
||||||
|
props: ["mask"],
|
||||||
|
}),
|
||||||
|
dropzone: createInput(FileDropzone, {
|
||||||
|
props: ["accept", "multiple", "maxSize", "minSize", "maxFiles", "disabled"],
|
||||||
|
}),
|
||||||
|
toggle: createInput(Toggle, {
|
||||||
|
props: ["onLabel", "offLabel"],
|
||||||
|
}),
|
||||||
|
};
|
||||||
94
assets/js/formkit-theme.js
Normal file
94
assets/js/formkit-theme.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// Create some re-useable definitions because
|
||||||
|
// many input types are identical in how
|
||||||
|
// we want to style them.
|
||||||
|
const textClassification = {
|
||||||
|
label: "formkit-outer-text",
|
||||||
|
inner: "formkit-inner-text",
|
||||||
|
input: "formkit-input-text",
|
||||||
|
prefix: "formkit-prefix-text",
|
||||||
|
message: "formkit-message-text",
|
||||||
|
};
|
||||||
|
const boxClassification = {
|
||||||
|
inner: "formkit-inner-box",
|
||||||
|
fieldset: "formkit-fieldset-box",
|
||||||
|
legend: "formkit-legend-box",
|
||||||
|
wrapper: "formkit-wrapper-box",
|
||||||
|
help: "formkit-help-box",
|
||||||
|
input: "formkit-input-box",
|
||||||
|
label: "formkit-label-box",
|
||||||
|
message: "formkit-message-box",
|
||||||
|
};
|
||||||
|
const buttonClassification = {
|
||||||
|
wrapper: "formkit-wrapper-button",
|
||||||
|
input: "formkit-input-button",
|
||||||
|
};
|
||||||
|
const OtpClassification = {
|
||||||
|
label: "formkit-label-otp",
|
||||||
|
inner: "formkit-inner-otp",
|
||||||
|
digit: "formkit-digit-otp",
|
||||||
|
message: "formkit-message-otp",
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorClassification = {
|
||||||
|
label: "formkit-label-color",
|
||||||
|
input: "formkit-input-color",
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileClassification = {
|
||||||
|
label: "formkit-label-file",
|
||||||
|
inner: "formkit-inner-file",
|
||||||
|
input: "formkit-input-file",
|
||||||
|
};
|
||||||
|
|
||||||
|
const rangeClassification = {
|
||||||
|
input: "formkit-input-range",
|
||||||
|
};
|
||||||
|
|
||||||
|
// export our definitions using our above
|
||||||
|
// templates and declare one-offs and
|
||||||
|
// overrides as needed.
|
||||||
|
export default {
|
||||||
|
// the global key will apply to all inputs
|
||||||
|
global: {
|
||||||
|
label: "formkit-label-global",
|
||||||
|
outer: "formkit-outer-global",
|
||||||
|
help: "formkit-help-global",
|
||||||
|
messages: "formkit-messages-global",
|
||||||
|
message: "formkit-message-global",
|
||||||
|
wrapper: "formkit-wrapper-global",
|
||||||
|
},
|
||||||
|
button: buttonClassification,
|
||||||
|
color: colorClassification,
|
||||||
|
date: textClassification,
|
||||||
|
"datetime-local": textClassification,
|
||||||
|
checkbox: boxClassification,
|
||||||
|
email: textClassification,
|
||||||
|
file: fileClassification,
|
||||||
|
month: textClassification,
|
||||||
|
number: textClassification,
|
||||||
|
password: textClassification,
|
||||||
|
radio: {
|
||||||
|
...boxClassification,
|
||||||
|
input: "formkit-input-radio",
|
||||||
|
},
|
||||||
|
range: rangeClassification,
|
||||||
|
search: textClassification,
|
||||||
|
select: { ...textClassification, option: "p-2" },
|
||||||
|
submit: buttonClassification,
|
||||||
|
tel: textClassification,
|
||||||
|
text: textClassification,
|
||||||
|
textarea: {
|
||||||
|
...textClassification,
|
||||||
|
input: "formkit-input-textarea",
|
||||||
|
},
|
||||||
|
time: textClassification,
|
||||||
|
url: textClassification,
|
||||||
|
week: textClassification,
|
||||||
|
otp: OtpClassification,
|
||||||
|
mask: textClassification,
|
||||||
|
dropzone: {
|
||||||
|
...textClassification,
|
||||||
|
inner: "formkit-inner-dropzone",
|
||||||
|
dropzone: "formkit-dropzone",
|
||||||
|
},
|
||||||
|
};
|
||||||
92
assets/json/data.json
Normal file
92
assets/json/data.json
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "#001",
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"email": "johndoe@example.com",
|
||||||
|
"gender": "Male",
|
||||||
|
"status": "Active",
|
||||||
|
"age": 34
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "#002",
|
||||||
|
"firstName": "Jane",
|
||||||
|
"lastName": "Smith",
|
||||||
|
"email": "janesmith@example.com",
|
||||||
|
"gender": "Female",
|
||||||
|
"status": "Inactive",
|
||||||
|
"age": 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "#003",
|
||||||
|
"firstName": "Robert",
|
||||||
|
"lastName": "Brown",
|
||||||
|
"email": "robertbrown@example.com",
|
||||||
|
"gender": "Male",
|
||||||
|
"status": "Banned",
|
||||||
|
"age": 45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "#004",
|
||||||
|
"firstName": "Emily",
|
||||||
|
"lastName": "White",
|
||||||
|
"email": "emilywhite@example.com",
|
||||||
|
"gender": "Female",
|
||||||
|
"status": "Active",
|
||||||
|
"age": 37
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "#005",
|
||||||
|
"firstName": "Michael",
|
||||||
|
"lastName": "Johnson",
|
||||||
|
"email": "michaeljohnson@example.com",
|
||||||
|
"gender": "Male",
|
||||||
|
"status": "Inactive",
|
||||||
|
"age": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "#006",
|
||||||
|
"firstName": "Linda",
|
||||||
|
"lastName": "Williams",
|
||||||
|
"email": "lindawilliams@example.com",
|
||||||
|
"gender": "Female",
|
||||||
|
"status": "Active",
|
||||||
|
"age": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "#007",
|
||||||
|
"firstName": "James",
|
||||||
|
"lastName": "Taylor",
|
||||||
|
"email": "jamestaylor@example.com",
|
||||||
|
"gender": "Male",
|
||||||
|
"status": "Banned",
|
||||||
|
"age": 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "#008",
|
||||||
|
"firstName": "Patricia",
|
||||||
|
"lastName": "Brown",
|
||||||
|
"email": "patriciabrown@example.com",
|
||||||
|
"gender": "Female",
|
||||||
|
"status": "Inactive",
|
||||||
|
"age": 29
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "#009",
|
||||||
|
"firstName": "David",
|
||||||
|
"lastName": "Wilson",
|
||||||
|
"email": "davidwilson@example.com",
|
||||||
|
"gender": "Male",
|
||||||
|
"status": "Active",
|
||||||
|
"age": 38
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "#010",
|
||||||
|
"firstName": "Elizabeth",
|
||||||
|
"lastName": "Garcia",
|
||||||
|
"email": "elizabethgarcia@example.com",
|
||||||
|
"gender": "Female",
|
||||||
|
"status": "Banned",
|
||||||
|
"age": 42
|
||||||
|
}
|
||||||
|
]
|
||||||
7126
assets/json/iconamoon.json
Normal file
7126
assets/json/iconamoon.json
Normal file
File diff suppressed because it is too large
Load Diff
268
assets/style/css/base/theme.css
Normal file
268
assets/style/css/base/theme.css
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
html[data-theme="biasa"] {
|
||||||
|
--color-primary: 0, 165, 154;
|
||||||
|
--color-secondary: 97, 176, 183;
|
||||||
|
--color-accent: 243, 244, 246;
|
||||||
|
--color-success: 79, 192, 103;
|
||||||
|
--color-info: 65, 133, 242;
|
||||||
|
--color-warning: 246, 141, 32;
|
||||||
|
--color-danger: 229, 83, 69;
|
||||||
|
--text-color: 9, 9, 11;
|
||||||
|
--border-color: 228, 228, 231;
|
||||||
|
--bg-1: 243, 244, 246;
|
||||||
|
--bg-2: 255, 255, 255;
|
||||||
|
--sidebar: 38, 50, 55;
|
||||||
|
--sidebar-menu: 26, 35, 38;
|
||||||
|
--sidebar-text: 255, 255, 255;
|
||||||
|
--header: 49, 65, 71;
|
||||||
|
--header-text: 255, 255, 255;
|
||||||
|
--scroll-color: 170, 170, 170;
|
||||||
|
--scroll-hover-color: 155, 155, 155;
|
||||||
|
--fk-border-color: 228, 228, 231;
|
||||||
|
--fk-placeholder-color: 146, 146, 153;
|
||||||
|
--box-shadow: rgba(36, 35, 71, 0.05) 0px 1px 1px,
|
||||||
|
rgba(36, 35, 71, 0.05) 0px 0px 1px 1px;
|
||||||
|
--cp-bg: 255, 255, 255;
|
||||||
|
--rounded-box: 0.2rem;
|
||||||
|
--rounded-btn: 0.2rem;
|
||||||
|
--rounded-badge: 1.9rem;
|
||||||
|
--animation-btn: 0.25s;
|
||||||
|
--animation-input: 0.2s;
|
||||||
|
--btn-text-case: uppercase;
|
||||||
|
--btn-focus-scale: 0.95;
|
||||||
|
--padding-btn: 0.5rem;
|
||||||
|
--border-btn: 1px;
|
||||||
|
--tab-border: 1px;
|
||||||
|
--tab-radius: 0.5rem;
|
||||||
|
--tw-shadow: #e5eaf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="gelap"] {
|
||||||
|
--color-primary: 97, 176, 183;
|
||||||
|
--color-secondary: 13, 27, 42;
|
||||||
|
--color-accent: 15, 23, 42;
|
||||||
|
--color-success: 79, 192, 103;
|
||||||
|
--color-info: 65, 133, 242;
|
||||||
|
--color-warning: 246, 141, 32;
|
||||||
|
--color-danger: 229, 83, 69;
|
||||||
|
--text-color: 209, 213, 219;
|
||||||
|
--border-color: 30, 41, 59;
|
||||||
|
--bg-1: 15, 23, 42;
|
||||||
|
--bg-2: 30, 41, 59;
|
||||||
|
--sidebar: 38, 50, 55;
|
||||||
|
--sidebar-menu: 26, 35, 38;
|
||||||
|
--sidebar-text: 255, 255, 255;
|
||||||
|
--header: 49, 65, 71;
|
||||||
|
--header-text: 255, 255, 255;
|
||||||
|
--scroll-color: 170, 170, 170;
|
||||||
|
--scroll-hover-color: 155, 155, 155;
|
||||||
|
--fk-border-color: 71, 85, 105;
|
||||||
|
--fk-placeholder-color: 71, 85, 105;
|
||||||
|
--box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||||
|
--cp-bg: 255, 255, 255;
|
||||||
|
--rounded-box: 0.5rem;
|
||||||
|
--rounded-btn: 0.5rem;
|
||||||
|
--rounded-badge: 1.9rem;
|
||||||
|
--animation-btn: 0.25s;
|
||||||
|
--animation-input: 0.2s;
|
||||||
|
--btn-text-case: uppercase;
|
||||||
|
--btn-focus-scale: 0.95;
|
||||||
|
--padding-btn: 0.625rem 1.5rem;
|
||||||
|
--border-btn: 1px;
|
||||||
|
--tab-border: 1px;
|
||||||
|
--tab-radius: 0.5rem;
|
||||||
|
--tw-shadow: #e5eaf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="biru"] {
|
||||||
|
--color-primary: 0, 102, 204;
|
||||||
|
--color-secondary: 51, 153, 255;
|
||||||
|
--color-accent: 255, 204, 0;
|
||||||
|
--color-success: 46, 204, 113;
|
||||||
|
--color-info: 52, 152, 219;
|
||||||
|
--color-warning: 246, 141, 32;
|
||||||
|
--color-danger: 231, 76, 60;
|
||||||
|
--text-color: 0, 0, 0;
|
||||||
|
--border-color: 200, 200, 200;
|
||||||
|
--bg-1: 240, 248, 255;
|
||||||
|
--bg-2: 230, 240, 250;
|
||||||
|
--sidebar: 38, 50, 55;
|
||||||
|
--sidebar-menu: 26, 35, 38;
|
||||||
|
--sidebar-text: 255, 255, 255;
|
||||||
|
--header: 49, 65, 71;
|
||||||
|
--header-text: 255, 255, 255;
|
||||||
|
--scroll-color: 180, 180, 180;
|
||||||
|
--scroll-hover-color: 150, 150, 150;
|
||||||
|
--fk-border-color: 200, 200, 200;
|
||||||
|
--fk-placeholder-color: 150, 150, 150;
|
||||||
|
--box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||||
|
--cp-bg: 255, 255, 255;
|
||||||
|
--rounded-box: 0.5rem;
|
||||||
|
--rounded-btn: 0.5rem;
|
||||||
|
--rounded-badge: 1.9rem;
|
||||||
|
--animation-btn: 0.25s;
|
||||||
|
--animation-input: 0.2s;
|
||||||
|
--btn-text-case: uppercase;
|
||||||
|
--btn-focus-scale: 0.95;
|
||||||
|
--padding-btn: 0.625rem 1.5rem;
|
||||||
|
--border-btn: 1px;
|
||||||
|
--tab-border: 1px;
|
||||||
|
--tab-radius: 0.5rem;
|
||||||
|
--tw-shadow: #e5eaf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="merah"] {
|
||||||
|
--color-primary: 204, 0, 0;
|
||||||
|
--color-secondary: 255, 102, 102;
|
||||||
|
--color-accent: 255, 255, 153;
|
||||||
|
--color-success: 46, 204, 113;
|
||||||
|
--color-info: 52, 152, 219;
|
||||||
|
--color-warning: 246, 141, 32;
|
||||||
|
--color-danger: 231, 76, 60;
|
||||||
|
--text-color: 0, 0, 0;
|
||||||
|
--border-color: 200, 200, 200;
|
||||||
|
--bg-1: 255, 240, 240;
|
||||||
|
--bg-2: 255, 230, 230;
|
||||||
|
--sidebar: 38, 50, 55;
|
||||||
|
--sidebar-menu: 26, 35, 38;
|
||||||
|
--sidebar-text: 255, 255, 255;
|
||||||
|
--header: 49, 65, 71;
|
||||||
|
--header-text: 255, 255, 255;
|
||||||
|
--scroll-color: 180, 180, 180;
|
||||||
|
--scroll-hover-color: 150, 150, 150;
|
||||||
|
--fk-border-color: 200, 200, 200;
|
||||||
|
--fk-placeholder-color: 150, 150, 150;
|
||||||
|
--box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||||
|
--cp-bg: 255, 255, 255;
|
||||||
|
--rounded-box: 0.5rem;
|
||||||
|
--rounded-btn: 0.5rem;
|
||||||
|
--rounded-badge: 1.9rem;
|
||||||
|
--animation-btn: 0.25s;
|
||||||
|
--animation-input: 0.2s;
|
||||||
|
--btn-text-case: uppercase;
|
||||||
|
--btn-focus-scale: 0.95;
|
||||||
|
--padding-btn: 0.625rem 1.5rem;
|
||||||
|
--border-btn: 1px;
|
||||||
|
--tab-border: 1px;
|
||||||
|
--tab-radius: 0.5rem;
|
||||||
|
--tw-shadow: #e5eaf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="ungu"] {
|
||||||
|
--color-primary: 75, 0, 130;
|
||||||
|
--color-secondary: 138, 43, 226;
|
||||||
|
--color-accent: 255, 215, 0;
|
||||||
|
--color-success: 46, 204, 113;
|
||||||
|
--color-info: 52, 152, 219;
|
||||||
|
--color-warning: 246, 141, 32;
|
||||||
|
--color-danger: 231, 76, 60;
|
||||||
|
--text-color: 0, 0, 0;
|
||||||
|
--border-color: 200, 200, 200;
|
||||||
|
--bg-1: 240, 248, 255;
|
||||||
|
--bg-2: 230, 240, 250;
|
||||||
|
--sidebar: 38, 50, 55;
|
||||||
|
--sidebar-menu: 26, 35, 38;
|
||||||
|
--sidebar-text: 255, 255, 255;
|
||||||
|
--header: 49, 65, 71;
|
||||||
|
--header-text: 255, 255, 255;
|
||||||
|
--scroll-color: 180, 180, 180;
|
||||||
|
--scroll-hover-color: 150, 150, 150;
|
||||||
|
--fk-border-color: 200, 200, 200;
|
||||||
|
--fk-placeholder-color: 150, 150, 150;
|
||||||
|
--box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||||
|
--cp-bg: 255, 255, 255;
|
||||||
|
--rounded-box: 0.5rem;
|
||||||
|
--rounded-btn: 0.5rem;
|
||||||
|
--rounded-badge: 1.9rem;
|
||||||
|
--animation-btn: 0.25s;
|
||||||
|
--animation-input: 0.2s;
|
||||||
|
--btn-text-case: uppercase;
|
||||||
|
--btn-focus-scale: 0.95;
|
||||||
|
--padding-btn: 0.625rem 1.5rem;
|
||||||
|
--border-btn: 1px;
|
||||||
|
--tab-border: 1px;
|
||||||
|
--tab-radius: 0.5rem;
|
||||||
|
--tw-shadow: #e5eaf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="oren"] {
|
||||||
|
--color-primary: 255, 103, 0;
|
||||||
|
--color-secondary: 255, 159, 64;
|
||||||
|
--color-accent: 0, 128, 128;
|
||||||
|
--color-success: 46, 204, 113;
|
||||||
|
--color-info: 52, 152, 219;
|
||||||
|
--color-warning: 246, 141, 32;
|
||||||
|
--color-danger: 231, 76, 60;
|
||||||
|
--text-color: 0, 0, 0;
|
||||||
|
--border-color: 200, 200, 200;
|
||||||
|
--bg-1: 255, 250, 240;
|
||||||
|
--bg-2: 255, 245, 230;
|
||||||
|
--sidebar: 38, 50, 55;
|
||||||
|
--sidebar-menu: 26, 35, 38;
|
||||||
|
--sidebar-text: 255, 255, 255;
|
||||||
|
--header: 49, 65, 71;
|
||||||
|
--header-text: 255, 255, 255;
|
||||||
|
--scroll-color: 180, 180, 180;
|
||||||
|
--scroll-hover-color: 150, 150, 150;
|
||||||
|
--fk-border-color: 200, 200, 200;
|
||||||
|
--fk-placeholder-color: 150, 150, 150;
|
||||||
|
--box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||||
|
--cp-bg: 255, 255, 255;
|
||||||
|
--rounded-box: 0.5rem;
|
||||||
|
--rounded-btn: 0.5rem;
|
||||||
|
--rounded-badge: 1.9rem;
|
||||||
|
--animation-btn: 0.25s;
|
||||||
|
--animation-input: 0.2s;
|
||||||
|
--btn-text-case: uppercase;
|
||||||
|
--btn-focus-scale: 0.95;
|
||||||
|
--padding-btn: 0.625rem 1.5rem;
|
||||||
|
--border-btn: 1px;
|
||||||
|
--tab-border: 1px;
|
||||||
|
--tab-radius: 0.5rem;
|
||||||
|
--tw-shadow: #e5eaf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="LZS"] {
|
||||||
|
--color-primary: 0, 90, 173; /* #005AAD - Blue */
|
||||||
|
--color-secondary: 141, 199, 61; /* #8DC73D - Green */
|
||||||
|
--color-accent: 255, 242, 0; /* #FFF200 - Yellow */
|
||||||
|
--color-success: 141, 199, 61; /* Using the green for success */
|
||||||
|
--color-info: 0, 90, 173; /* Using the blue for info */
|
||||||
|
--color-warning: 246, 141, 32; /* Using consistent orange for warning */
|
||||||
|
--color-danger: 229, 83, 69; /* Keeping original red for danger */
|
||||||
|
--text-color: 0, 0, 0; /* Black text */
|
||||||
|
--border-color: 220, 220, 220; /* Light gray for borders */
|
||||||
|
--bg-1: 245, 250, 255; /* Very light blue background */
|
||||||
|
--bg-2: 255, 255, 255; /* White background */
|
||||||
|
--sidebar: 0, 58, 112; /* Darker blue for sidebar - #003A70 */
|
||||||
|
--sidebar-menu: 0, 40, 77; /* Even darker blue for sidebar menu - #00284D */
|
||||||
|
--sidebar-text: 255, 255, 255; /* White text for sidebar */
|
||||||
|
--header: 0, 90, 173; /* Blue header - #005AAD */
|
||||||
|
--header-text: 255, 255, 255; /* White text for header */
|
||||||
|
--scroll-color: 180, 180, 180; /* Gray scrollbar */
|
||||||
|
--scroll-hover-color: 150, 150, 150; /* Darker gray on hover */
|
||||||
|
--fk-border-color: 220, 220, 220; /* Light gray for form borders */
|
||||||
|
--fk-placeholder-color: 150, 150, 150; /* Gray for placeholders */
|
||||||
|
--box-shadow: rgba(0, 90, 173, 0.1) 0px 1px 2px,
|
||||||
|
rgba(0, 90, 173, 0.08) 0px 0px 2px; /* Blue-tinted shadow */
|
||||||
|
--cp-bg: 255, 255, 255; /* White background */
|
||||||
|
--rounded-box: 0.5rem;
|
||||||
|
--rounded-btn: 0.5rem;
|
||||||
|
--rounded-badge: 1.9rem;
|
||||||
|
--animation-btn: 0.25s;
|
||||||
|
--animation-input: 0.2s;
|
||||||
|
--btn-text-case: uppercase;
|
||||||
|
--btn-focus-scale: 0.95;
|
||||||
|
--padding-btn: 0.625rem 1.5rem;
|
||||||
|
--border-btn: 1px;
|
||||||
|
--tab-border: 1px;
|
||||||
|
--tab-radius: 0.5rem;
|
||||||
|
/* Yellow accents in specific UI elements */
|
||||||
|
--active-menu-bg: 255, 242, 0, 0.1; /* Subtle yellow background for active menu items */
|
||||||
|
--active-menu-border: 255, 242, 0; /* Yellow border for active menu items */
|
||||||
|
--focus-ring: 255, 242, 0, 0.5; /* Yellow focus ring */
|
||||||
|
--tw-shadow: #e5eaf2;
|
||||||
|
}
|
||||||
28
assets/style/css/component/alert.css
Normal file
28
assets/style/css/component/alert.css
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/* RS Alert Component */
|
||||||
|
.alert {
|
||||||
|
@apply visible flex items-center justify-between py-3 px-3 rounded-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.alert-primary {
|
||||||
|
@apply bg-primary/20 text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.alert-secondary {
|
||||||
|
@apply bg-secondary/20 text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.alert-success {
|
||||||
|
@apply bg-success/20 text-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.alert-info {
|
||||||
|
@apply bg-info/20 text-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.alert-warning {
|
||||||
|
@apply bg-warning/20 text-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.alert-danger {
|
||||||
|
@apply bg-danger/20 text-danger;
|
||||||
|
}
|
||||||
32
assets/style/css/component/badge.css
Normal file
32
assets/style/css/component/badge.css
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/* RS Badge Component */
|
||||||
|
.badge {
|
||||||
|
@apply inline-flex items-center justify-center px-2 py-1 rounded-full text-xs font-semibold leading-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.badge-primary {
|
||||||
|
@apply bg-primary text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.badge-secondary {
|
||||||
|
@apply bg-secondary text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.badge-success {
|
||||||
|
@apply bg-success text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.badge-info {
|
||||||
|
@apply bg-info text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.badge-warning {
|
||||||
|
@apply bg-warning text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.badge-danger {
|
||||||
|
@apply bg-danger text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.badge-disabled {
|
||||||
|
@apply bg-gray-300 text-gray-600;
|
||||||
|
}
|
||||||
323
assets/style/css/component/button.css
Normal file
323
assets/style/css/component/button.css
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
/* RS Button */
|
||||||
|
.button {
|
||||||
|
@apply w-fit rounded-lg flex justify-center items-center h-fit;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced hover effect with slight 3D transition */
|
||||||
|
.button[class*="button-"]:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button[class*="button-"]:active {
|
||||||
|
transform: translateY(0px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button[class*="button-"]:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primary Button - Blue with yellow accent */
|
||||||
|
.button.button-primary {
|
||||||
|
@apply bg-primary text-white;
|
||||||
|
box-shadow: 0 2px 4px rgba(var(--color-primary), 0.3),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
|
border: 1px solid rgba(var(--color-primary), 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-primary:hover::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background-color: rgb(var(--active-menu-border));
|
||||||
|
animation: slide-in 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-in {
|
||||||
|
0% { transform: scaleX(0); opacity: 0; }
|
||||||
|
100% { transform: scaleX(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-secondary {
|
||||||
|
@apply bg-secondary text-white;
|
||||||
|
box-shadow: 0 2px 4px rgba(var(--color-secondary), 0.3),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
|
border: 1px solid rgba(var(--color-secondary), 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-success {
|
||||||
|
@apply bg-success text-white;
|
||||||
|
box-shadow: 0 2px 4px rgba(var(--color-success), 0.3),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
|
border: 1px solid rgba(var(--color-success), 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-info {
|
||||||
|
@apply bg-info text-white;
|
||||||
|
box-shadow: 0 2px 4px rgba(var(--color-info), 0.3),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
|
border: 1px solid rgba(var(--color-info), 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-warning {
|
||||||
|
@apply bg-warning text-white;
|
||||||
|
box-shadow: 0 2px 4px rgba(var(--color-warning), 0.3),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
|
border: 1px solid rgba(var(--color-warning), 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-danger {
|
||||||
|
@apply bg-danger text-white;
|
||||||
|
box-shadow: 0 2px 4px rgba(var(--color-danger), 0.3),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
|
border: 1px solid rgba(var(--color-danger), 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Updated outline buttons */
|
||||||
|
.button[class*="outline-"]:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.outline-primary {
|
||||||
|
@apply border border-primary text-primary;
|
||||||
|
box-shadow: 0 1px 3px rgba(var(--color-primary), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.outline-primary:hover {
|
||||||
|
@apply bg-primary/5;
|
||||||
|
box-shadow: 0 3px 6px rgba(var(--color-primary), 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.outline-secondary {
|
||||||
|
@apply border border-secondary text-secondary;
|
||||||
|
box-shadow: 0 1px 3px rgba(var(--color-secondary), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.outline-secondary:hover {
|
||||||
|
@apply bg-secondary/5;
|
||||||
|
box-shadow: 0 3px 6px rgba(var(--color-secondary), 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.outline-success {
|
||||||
|
@apply border border-success text-success;
|
||||||
|
box-shadow: 0 1px 3px rgba(var(--color-success), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.outline-success:hover {
|
||||||
|
@apply bg-success/5;
|
||||||
|
box-shadow: 0 3px 6px rgba(var(--color-success), 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.outline-info {
|
||||||
|
@apply border border-info text-info;
|
||||||
|
box-shadow: 0 1px 3px rgba(var(--color-info), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.outline-info:hover {
|
||||||
|
@apply bg-info/5;
|
||||||
|
box-shadow: 0 3px 6px rgba(var(--color-info), 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.outline-warning {
|
||||||
|
@apply border border-warning text-warning;
|
||||||
|
box-shadow: 0 1px 3px rgba(var(--color-warning), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.outline-warning:hover {
|
||||||
|
@apply bg-warning/5;
|
||||||
|
box-shadow: 0 3px 6px rgba(var(--color-warning), 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.outline-danger {
|
||||||
|
@apply border border-danger text-danger;
|
||||||
|
box-shadow: 0 1px 3px rgba(var(--color-danger), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.outline-danger:hover {
|
||||||
|
@apply bg-danger/5;
|
||||||
|
box-shadow: 0 3px 6px rgba(var(--color-danger), 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button[class*="text-"]:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-primary {
|
||||||
|
@apply text-primary;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-primary:hover {
|
||||||
|
@apply bg-primary/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-primary:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: rgb(var(--color-primary));
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-primary:hover:after {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-secondary {
|
||||||
|
@apply text-secondary;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-secondary:hover {
|
||||||
|
@apply bg-secondary/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-secondary:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: rgb(var(--color-secondary));
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-secondary:hover:after {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-success {
|
||||||
|
@apply text-success;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-success:hover {
|
||||||
|
@apply bg-success/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-success:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: rgb(var(--color-success));
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-success:hover:after {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-info {
|
||||||
|
@apply text-info;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-info:hover {
|
||||||
|
@apply bg-info/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-info:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: rgb(var(--color-info));
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-info:hover:after {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-warning {
|
||||||
|
@apply text-warning;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-warning:hover {
|
||||||
|
@apply bg-warning/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-warning:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: rgb(var(--color-warning));
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-warning:hover:after {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-danger {
|
||||||
|
@apply text-danger;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-danger:hover {
|
||||||
|
@apply bg-danger/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-danger:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: rgb(var(--color-danger));
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.texts-danger:hover:after {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-sm {
|
||||||
|
@apply text-xs;
|
||||||
|
padding: var(--padding-btn);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-md {
|
||||||
|
@apply text-sm;
|
||||||
|
padding: var(--padding-btn);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-lg {
|
||||||
|
@apply text-base;
|
||||||
|
padding: var(--padding-btn);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
24
assets/style/css/component/card.css
Normal file
24
assets/style/css/component/card.css
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/* RS Card Component */
|
||||||
|
.card {
|
||||||
|
@apply mb-6;
|
||||||
|
background-color: rgb(var(--bg-2));
|
||||||
|
border-radius: var(--rounded-box);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-header {
|
||||||
|
@apply text-xl px-6 py-4 font-medium;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-bottom: 1px solid rgb(var(--border-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-body {
|
||||||
|
@apply px-6 py-5;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-footer {
|
||||||
|
@apply px-6 py-4;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-top: 1px solid rgb(var(--border-color));
|
||||||
|
}
|
||||||
61
assets/style/css/component/collapse.css
Normal file
61
assets/style/css/component/collapse.css
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/* RS Collapse Component */
|
||||||
|
.accordion {
|
||||||
|
@apply w-full mb-4 visible;
|
||||||
|
background-color: rgb(var(--bg-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion.accordion-border {
|
||||||
|
@apply border-t border-x rounded-lg;
|
||||||
|
border-color: rgb(var(--border-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion .accordion-group {
|
||||||
|
@apply overflow-hidden duration-300 dark:border-slate-700;
|
||||||
|
background-color: rgb(var(--bg-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion .accordion-group.accordion-default {
|
||||||
|
@apply border-b;
|
||||||
|
border-color: rgb(var(--border-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion .accordion-group.accordion-border {
|
||||||
|
@apply border-b last:rounded-b-lg;
|
||||||
|
border-color: rgb(var(--border-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion .accordion-group.accordion-card {
|
||||||
|
@apply my-4;
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion .accordion-group .accordion-header {
|
||||||
|
@apply text-lg font-medium px-5 pr-10 py-4 cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion .accordion-group .accordion-body {
|
||||||
|
@apply px-5 pb-4 pt-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-group .accordion-header {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-group .accordion-header::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
top: 50%;
|
||||||
|
right: 0%;
|
||||||
|
transform: translate(-100%, -50%) rotate(0deg);
|
||||||
|
background: url("/assets/img/icon/chevron-right.svg");
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
transition: 0.25s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-group--open .accordion-header::after {
|
||||||
|
transform: translate(-100%, -50%) rotate(90deg);
|
||||||
|
}
|
||||||
40
assets/style/css/component/common.css
Normal file
40
assets/style/css/component/common.css
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
body {
|
||||||
|
color: rgb(var(--text-color));
|
||||||
|
background-color: rgb(var(--bg-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-header {
|
||||||
|
@apply z-20 fixed top-0 right-0 px-5 py-3 duration-300 shadow-md;
|
||||||
|
background-color: rgb(var(--header));
|
||||||
|
color: rgb(var(--header-text));
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-header-search {
|
||||||
|
@apply px-4 z-40 duration-300 shadow-md -top-20 focus-within:top-0 right-0;
|
||||||
|
background-color: rgb(var(--bg-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-menu {
|
||||||
|
@apply text-base h-screen fixed w-64 top-0 z-50 duration-300 border-l-0 shadow-md;
|
||||||
|
background-color: rgb(var(--sidebar));
|
||||||
|
color: rgb(var(--sidebar-text));
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
@apply flex
|
||||||
|
items-center
|
||||||
|
justify-center
|
||||||
|
transition-colors
|
||||||
|
duration-300;
|
||||||
|
color: rgb(var(--header-text));
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn.profile {
|
||||||
|
color: rgb(var(--header-text));
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn:hover {
|
||||||
|
background-color: rgb(var(--sidebar));
|
||||||
|
}
|
||||||
53
assets/style/css/component/dropdown.css
Normal file
53
assets/style/css/component/dropdown.css
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/* RS Dropdown Component */
|
||||||
|
.dropdown {
|
||||||
|
@apply relative inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown .dropdown-section {
|
||||||
|
@apply absolute z-10 shadow-md rounded-lg py-1 whitespace-nowrap;
|
||||||
|
background-color: rgb(var(--bg-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section.list-bottom-sm {
|
||||||
|
@apply top-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section.list-bottom-md {
|
||||||
|
@apply top-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section.list-bottom-lg {
|
||||||
|
@apply top-16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section.list-top-sm {
|
||||||
|
@apply bottom-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section.list-top-md {
|
||||||
|
@apply bottom-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section.list-top-lg {
|
||||||
|
@apply bottom-16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section.list-left {
|
||||||
|
@apply top-0 -left-[10.5rem];
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section.list-right {
|
||||||
|
@apply top-0 -right-[10.5rem];
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section.list-align-right {
|
||||||
|
@apply right-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section .dropdown-item {
|
||||||
|
@apply flex items-center cursor-pointer px-4 py-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section .dropdown-item:hover {
|
||||||
|
background-color: rgb(var(--bg-1));
|
||||||
|
}
|
||||||
50
assets/style/css/component/modal.css
Normal file
50
assets/style/css/component/modal.css
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/* RS Modal Component */
|
||||||
|
.modal {
|
||||||
|
@apply fixed top-0 left-0 w-full h-full overflow-hidden z-[1000] duration-300;
|
||||||
|
background: rgba(15, 23, 42, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.modal-top {
|
||||||
|
@apply flex items-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.modal-center {
|
||||||
|
@apply flex items-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.modal-end {
|
||||||
|
@apply flex items-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.modal-hide-overlay {
|
||||||
|
@apply !bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .modal-dialog {
|
||||||
|
@apply w-full md:w-auto relative z-[9999];
|
||||||
|
margin: 1.75rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .modal-dialog .modal-content {
|
||||||
|
@apply border-none shadow-lg relative flex flex-col w-full pointer-events-auto bg-clip-padding rounded-md outline-none text-current;
|
||||||
|
background-color: rgb(var(--bg-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .modal-dialog .modal-content .modal-header {
|
||||||
|
@apply flex flex-shrink-0 items-center justify-between p-4 border-b rounded-t-md;
|
||||||
|
border-color: rgb(var(--border-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .modal-dialog .modal-content .modal-body {
|
||||||
|
@apply relative p-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .modal-dialog .modal-content .modal-footer {
|
||||||
|
@apply flex flex-shrink-0 flex-wrap items-center justify-end px-4 pb-4 rounded-b-md gap-x-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.modal-dialog {
|
||||||
|
margin: 1.75rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
80
assets/style/css/component/progress.css
Normal file
80
assets/style/css/component/progress.css
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/* RS Progress Component */
|
||||||
|
.progress-wrapper {
|
||||||
|
@apply w-full mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress-label {
|
||||||
|
@apply text-sm mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress {
|
||||||
|
@apply flex rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress.progress-sm {
|
||||||
|
@apply h-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress.progress-md {
|
||||||
|
@apply h-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress.progress-lg {
|
||||||
|
@apply h-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress.progress-sm {
|
||||||
|
@apply h-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress.progress-primary {
|
||||||
|
@apply bg-primary/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress.progress-secondary {
|
||||||
|
@apply bg-secondary/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress.progress-success {
|
||||||
|
@apply bg-success/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress.progress-info {
|
||||||
|
@apply bg-info/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress.progress-warning {
|
||||||
|
@apply bg-warning/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress.progress-danger {
|
||||||
|
@apply bg-danger/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress .progress-bar {
|
||||||
|
@apply flex items-center justify-center rounded-full text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress .progress-bar.primary {
|
||||||
|
@apply bg-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress .progress-bar.secondary {
|
||||||
|
@apply bg-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress .progress-bar.success {
|
||||||
|
@apply bg-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress .progress-bar.info {
|
||||||
|
@apply bg-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress .progress-bar.warning {
|
||||||
|
@apply bg-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper .progress .progress-bar.danger {
|
||||||
|
@apply bg-danger;
|
||||||
|
}
|
||||||
322
assets/style/css/component/tab.css
Normal file
322
assets/style/css/component/tab.css
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
/* RS Tab Component */
|
||||||
|
.tab {
|
||||||
|
@apply rounded-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.vertical {
|
||||||
|
@apply flex flex-col md:flex-row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.tab-card {
|
||||||
|
@apply shadow-md pt-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.card-vertical {
|
||||||
|
@apply shadow-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.card-primary {
|
||||||
|
@apply bg-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.card-secondary {
|
||||||
|
@apply bg-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.card-success {
|
||||||
|
@apply bg-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.card-info {
|
||||||
|
@apply bg-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.card-warning {
|
||||||
|
@apply bg-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.card-danger {
|
||||||
|
@apply bg-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav {
|
||||||
|
@apply flex flex-wrap list-none pl-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav.tab-nav-card {
|
||||||
|
@apply mx-4 mb-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav.card-vertical {
|
||||||
|
@apply py-0 pt-4 md:py-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav.vertical {
|
||||||
|
@apply flex-row md:flex-col gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav.vertical-fill {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item {
|
||||||
|
@apply cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.fill {
|
||||||
|
@apply flex-1 w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-horizontal {
|
||||||
|
@apply border-0 hover:border hover:border-b-0 rounded-t-md px-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-horizontal-active {
|
||||||
|
@apply border rounded-t-md border-b-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-vertical {
|
||||||
|
@apply border-0 hover:border hover:border-r-0 rounded-l-md px-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-vertical-active {
|
||||||
|
@apply border rounded-t-md rounded-bl-none md:rounded-r-none md:rounded-l-md border-r border-b-0 md:border-b md:border-r-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-hover-primary {
|
||||||
|
@apply hover:border-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-hover-secondary {
|
||||||
|
@apply hover:border-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-hover-success {
|
||||||
|
@apply hover:border-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-hover-info {
|
||||||
|
@apply hover:border-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-hover-warning {
|
||||||
|
@apply hover:border-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-hover-danger {
|
||||||
|
@apply hover:border-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-active-primary {
|
||||||
|
@apply border-primary text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-active-secondary {
|
||||||
|
@apply border-secondary text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-active-success {
|
||||||
|
@apply border-success text-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-active-info {
|
||||||
|
@apply border-info text-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-active-warning {
|
||||||
|
@apply border-warning text-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item.border-active-danger {
|
||||||
|
@apply border-danger text-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link {
|
||||||
|
@apply block font-medium text-base leading-tight capitalize border-x-0 border-t-0 py-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default {
|
||||||
|
@apply hover:border-b-2 mx-2 px-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default-vertical {
|
||||||
|
@apply hover:border-l-2 mx-2 px-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default-active {
|
||||||
|
@apply border-b-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default-vertical-active {
|
||||||
|
@apply border-l-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default-hover-primary {
|
||||||
|
@apply hover:border-primary hover:text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default-hover-secondary {
|
||||||
|
@apply hover:border-secondary hover:text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default-hover-success {
|
||||||
|
@apply hover:border-success hover:text-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default-hover-info {
|
||||||
|
@apply hover:border-info hover:text-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default-hover-warning {
|
||||||
|
@apply hover:border-warning hover:text-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default-hover-danger {
|
||||||
|
@apply hover:border-danger hover:text-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default-primary {
|
||||||
|
@apply text-primary border-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default-secondary {
|
||||||
|
@apply text-secondary border-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default-success {
|
||||||
|
@apply text-success border-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default-info {
|
||||||
|
@apply text-info border-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default-warning {
|
||||||
|
@apply text-warning border-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.default-danger {
|
||||||
|
@apply text-danger border-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-card {
|
||||||
|
@apply px-5 mx-1 text-white rounded-t-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-card-vertical {
|
||||||
|
@apply px-5 my-0 text-white rounded-bl-none rounded-t-md md:rounded-tr-none md:rounded-l-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-card-primary {
|
||||||
|
@apply bg-primary/90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-card-secondary {
|
||||||
|
@apply bg-secondary/90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-card-success {
|
||||||
|
@apply bg-success/90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-card-info {
|
||||||
|
@apply bg-info/90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-card-warning {
|
||||||
|
@apply bg-warning/90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-card-danger {
|
||||||
|
@apply bg-danger/90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-card-primary-active {
|
||||||
|
@apply text-primary/90;
|
||||||
|
background-color: rgb(var(--bg-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-card-secondary-active {
|
||||||
|
@apply text-secondary/90;
|
||||||
|
background-color: rgb(var(--bg-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-card-success-active {
|
||||||
|
@apply text-success/90;
|
||||||
|
background-color: rgb(var(--bg-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-card-info-active {
|
||||||
|
@apply text-info/90;
|
||||||
|
background-color: rgb(var(--bg-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-card-warning-active {
|
||||||
|
@apply text-warning/90;
|
||||||
|
background-color: rgb(var(--bg-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-card-danger-active {
|
||||||
|
@apply text-danger/90;
|
||||||
|
background-color: rgb(var(--bg-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-justify-left {
|
||||||
|
@apply flex flex-wrap justify-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-justify-center {
|
||||||
|
@apply flex flex-wrap justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-nav .tab-item .tab-item-link.link-justify-right {
|
||||||
|
@apply flex flex-wrap justify-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-content {
|
||||||
|
background-color: rgb(var(--bg-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-content.content-vertical {
|
||||||
|
@apply flex-grow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-content.content-vertical-fill {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-content.content-border {
|
||||||
|
@apply border rounded-b-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-content.content-border-vertical {
|
||||||
|
@apply border rounded-md rounded-l-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-content.content-border-primary {
|
||||||
|
@apply border-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-content.content-border-secondary {
|
||||||
|
@apply border-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-content.content-border-success {
|
||||||
|
@apply border-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-content.content-border-info {
|
||||||
|
@apply border-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-content.content-border-warning {
|
||||||
|
@apply border-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-content.content-border-danger {
|
||||||
|
@apply border-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .tab-content .tab-pane {
|
||||||
|
@apply py-4 px-4;
|
||||||
|
}
|
||||||
32
assets/style/css/component/table.css
Normal file
32
assets/style/css/component/table.css
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/* Table Component */
|
||||||
|
.table-wrapper {
|
||||||
|
@apply w-full border-0 rounded-md border-gray-200 dark:border-slate-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
@apply m-4 overflow-hidden;
|
||||||
|
transition: max-height 0.25s cubic-bezier(0, 1, 0, 1);
|
||||||
|
max-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header.open {
|
||||||
|
@apply overflow-visible;
|
||||||
|
transition: max-height 0.5s ease-in-out;
|
||||||
|
max-height: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header-filter {
|
||||||
|
@apply flex flex-col md:flex-row justify-start md:justify-between items-start md:items-center gap-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-content {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-footer {
|
||||||
|
@apply flex justify-center md:justify-between items-center m-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-footer-page {
|
||||||
|
@apply flex justify-center gap-x-2;
|
||||||
|
}
|
||||||
65
assets/style/css/example-theme.css
Normal file
65
assets/style/css/example-theme.css
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/* Example Custom Theme for corradAF Base Project */
|
||||||
|
/* This file demonstrates how custom themes should be structured */
|
||||||
|
/* Define your custom theme variables here */
|
||||||
|
/* For example:
|
||||||
|
:root {
|
||||||
|
--primary-color: #yourColor;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Custom color variables */
|
||||||
|
--color-primary: 46, 125, 50; /* Green theme */
|
||||||
|
--color-secondary: 117, 117, 117;
|
||||||
|
--color-success: 76, 175, 80;
|
||||||
|
--color-info: 33, 150, 243;
|
||||||
|
--color-warning: 255, 152, 0;
|
||||||
|
--color-danger: 244, 67, 54;
|
||||||
|
|
||||||
|
/* Custom background colors */
|
||||||
|
--bg-primary: 245, 245, 245;
|
||||||
|
--bg-secondary: 255, 255, 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme overrides */
|
||||||
|
.dark {
|
||||||
|
--bg-primary: 18, 18, 18;
|
||||||
|
--bg-secondary: 30, 30, 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom component styles */
|
||||||
|
.btn-primary {
|
||||||
|
background-color: rgb(var(--color-primary));
|
||||||
|
border-color: rgb(var(--color-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: rgba(var(--color-primary), 0.8);
|
||||||
|
border-color: rgba(var(--color-primary), 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header customizations */
|
||||||
|
.w-header {
|
||||||
|
background: linear-gradient(135deg, rgb(var(--color-primary)), rgba(var(--color-primary), 0.8));
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar customizations */
|
||||||
|
.vertical-menu {
|
||||||
|
background-color: rgb(var(--bg-secondary));
|
||||||
|
border-right: 1px solid rgba(var(--color-primary), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card customizations */
|
||||||
|
.card {
|
||||||
|
background-color: rgb(var(--bg-secondary));
|
||||||
|
border: 1px solid rgba(var(--color-primary), 0.1);
|
||||||
|
box-shadow: 0 2px 4px rgba(var(--color-primary), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Example of responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.w-header {
|
||||||
|
background: rgb(var(--color-primary));
|
||||||
|
}
|
||||||
|
}
|
||||||
63
assets/style/css/form/box.css
Normal file
63
assets/style/css/form/box.css
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
.formkit-inner-box {
|
||||||
|
@apply relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-label {
|
||||||
|
@apply block mb-2 font-semibold text-sm formkit-invalid:text-red-500 dark:formkit-invalid:text-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-fieldset-box {
|
||||||
|
@apply max-w-md border border-[rgb(var(--fk-border-color))] rounded-lg px-4 py-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-legend-box {
|
||||||
|
@apply font-bold text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-wrapper-box {
|
||||||
|
@apply flex items-center mb-3 cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-help-box {
|
||||||
|
@apply mb-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-input-box {
|
||||||
|
@apply flex
|
||||||
|
items-center
|
||||||
|
hover:cursor-pointer
|
||||||
|
appearance-none
|
||||||
|
h-5 w-5 mr-2
|
||||||
|
border-2
|
||||||
|
border-[rgb(var(--fk-border-color))]
|
||||||
|
checked:bg-primary
|
||||||
|
checked:border-transparent
|
||||||
|
bg-[rgb(var(--bg-2))]
|
||||||
|
rounded-md
|
||||||
|
checked:shadow-sm checked:shadow-primary/40
|
||||||
|
focus:outline-none focus:ring-0 transition duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-input-radio {
|
||||||
|
@apply flex
|
||||||
|
items-center
|
||||||
|
hover:cursor-pointer
|
||||||
|
appearance-none
|
||||||
|
h-5 w-5 mr-2
|
||||||
|
border-2
|
||||||
|
border-[rgb(var(--fk-border-color))]
|
||||||
|
checked:bg-primary
|
||||||
|
checked:border-transparent
|
||||||
|
bg-[rgb(var(--bg-2))]
|
||||||
|
rounded-full
|
||||||
|
checked:shadow-sm checked:shadow-primary/40
|
||||||
|
focus:outline-none focus:ring-0 transition duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-label-box {
|
||||||
|
@apply text-sm formkit-disabled:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-message-box {
|
||||||
|
@apply formkit-invalid:text-red-500;
|
||||||
|
}
|
||||||
7
assets/style/css/form/button.css
Normal file
7
assets/style/css/form/button.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.formkit-wrapper-button {
|
||||||
|
@apply mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-input-button {
|
||||||
|
@apply bg-primary hover:bg-primary/90 text-white text-sm font-normal py-2 px-5 rounded-lg;
|
||||||
|
}
|
||||||
18
assets/style/css/form/color.css
Normal file
18
assets/style/css/form/color.css
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.formkit-label-color {
|
||||||
|
@apply block mb-1 font-bold text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-input-color {
|
||||||
|
@apply w-16 h-10 cursor-pointer rounded-lg mb-2 border-none appearance-none bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-input-color::-webkit-color-swatch {
|
||||||
|
border-radius: 5px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||||
|
}
|
||||||
|
.formkit-input-color::-moz-color-swatch {
|
||||||
|
border-radius: 5px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||||
|
}
|
||||||
7
assets/style/css/form/dropzone.css
Normal file
7
assets/style/css/form/dropzone.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.formkit-inner-dropzone {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-dropzone {
|
||||||
|
@apply border-2 border-[rgb(var(--fk-border-color))] border-dashed p-6 active:bg-[rgb(var(--bg-1))];
|
||||||
|
}
|
||||||
32
assets/style/css/form/file.css
Normal file
32
assets/style/css/form/file.css
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
.formkit-label-file {
|
||||||
|
@apply block mb-1 font-bold text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-inner-file {
|
||||||
|
@apply w-full cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-input-file {
|
||||||
|
@apply w-full cursor-pointer border rounded-lg text-gray-600 text-sm mb-1 file:cursor-pointer file:mr-4 file:py-2 file:px-4 file:rounded-l-lg file:border-0 file:text-sm file:bg-primary file:text-white hover:file:bg-primary/90;
|
||||||
|
border-color: rgb(var(--fk-border-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-file-list {
|
||||||
|
@apply flex flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-file-item {
|
||||||
|
@apply flex items-center py-2 px-4 rounded-lg border border-gray-200 mb-1 mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-file-name {
|
||||||
|
@apply text-[rgb(var(--text-color))] text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-file-remove {
|
||||||
|
@apply ml-auto text-primary text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-no-files {
|
||||||
|
@apply text-[rgb(var(--text-color))] text-sm;
|
||||||
|
}
|
||||||
23
assets/style/css/form/global.css
Normal file
23
assets/style/css/form/global.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
.formkit-label-global {
|
||||||
|
@apply text-[rgb(var(--text-color))];
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-outer-global {
|
||||||
|
@apply mb-4 text-[rgb(var(--text-color))] formkit-disabled:opacity-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-help-global {
|
||||||
|
@apply text-xs mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-messages-global {
|
||||||
|
@apply list-none p-0 mt-1 mb-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-message-global {
|
||||||
|
@apply text-red-500 mb-1 text-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-wrapper-global {
|
||||||
|
@apply relative;
|
||||||
|
}
|
||||||
29
assets/style/css/form/otp.css
Normal file
29
assets/style/css/form/otp.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.formkit-label-otp {
|
||||||
|
@apply block mb-2 font-semibold text-sm formkit-invalid:text-red-500 dark:formkit-invalid:text-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-inner-otp {
|
||||||
|
@apply flex
|
||||||
|
items-center
|
||||||
|
justify-start
|
||||||
|
align-middle
|
||||||
|
rounded-lg mb-1
|
||||||
|
overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-digit-otp {
|
||||||
|
@apply w-10 h-10 mr-2
|
||||||
|
text-center
|
||||||
|
rounded-lg
|
||||||
|
border
|
||||||
|
border-[rgb(var(--fk-border-color))]
|
||||||
|
text-sm
|
||||||
|
bg-[rgb(var(--bg-2))]
|
||||||
|
placeholder-secondary
|
||||||
|
focus-within:border-primary
|
||||||
|
focus:outline-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-message-otp {
|
||||||
|
@apply formkit-invalid:text-red-500 dark:formkit-invalid:text-danger;
|
||||||
|
}
|
||||||
3
assets/style/css/form/range.css
Normal file
3
assets/style/css/form/range.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.formkit-input-range {
|
||||||
|
@apply appearance-none w-full h-2 p-0 bg-[rgb(var(--bg-1))] rounded-full focus:outline-none focus:ring-0 focus:shadow-none;
|
||||||
|
}
|
||||||
39
assets/style/css/form/text.css
Normal file
39
assets/style/css/form/text.css
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
.formkit-inner-text {
|
||||||
|
@apply flex
|
||||||
|
items-center
|
||||||
|
justify-center
|
||||||
|
align-middle
|
||||||
|
w-full
|
||||||
|
border
|
||||||
|
border-[rgb(var(--fk-border-color))]
|
||||||
|
formkit-invalid:border-red-500
|
||||||
|
rounded-lg
|
||||||
|
overflow-hidden
|
||||||
|
focus-within:border-primary
|
||||||
|
mb-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-input-text {
|
||||||
|
@apply w-full
|
||||||
|
h-10
|
||||||
|
px-3
|
||||||
|
border-none
|
||||||
|
text-sm
|
||||||
|
bg-[rgb(var(--bg-2))]
|
||||||
|
placeholder-[rgb(var(--fk-placeholder-color))]
|
||||||
|
focus:outline-none
|
||||||
|
disabled:bg-[rgb(var(--bg-1))]
|
||||||
|
disabled:border-[rgb(var(--bg-1))]
|
||||||
|
disabled:placeholder-[rgb(var(--bg-1))]
|
||||||
|
read-only:bg-[rgb(var(--bg-1))]
|
||||||
|
read-only:border-[rgb(var(--bg-1))]
|
||||||
|
read-only:placeholder-[rgb(var(--bg-1))];
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-prefix-text {
|
||||||
|
@apply ml-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-message-text {
|
||||||
|
@apply formkit-invalid:text-red-500;
|
||||||
|
}
|
||||||
8
assets/style/css/form/textarea.css
Normal file
8
assets/style/css/form/textarea.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.formkit-input-textarea {
|
||||||
|
@apply block
|
||||||
|
w-full
|
||||||
|
px-3 py-2
|
||||||
|
placeholder-[rgb(var(--fk-placeholder-color))]
|
||||||
|
bg-[rgb(var(--bg-2))]
|
||||||
|
focus:outline-none;
|
||||||
|
}
|
||||||
33
assets/style/css/tailwind.css
Normal file
33
assets/style/css/tailwind.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/* Base Import Tailwind CSS */
|
||||||
|
@import "tailwindcss/base";
|
||||||
|
@import "./base/theme";
|
||||||
|
|
||||||
|
/* Components Import Tailwind CSS */
|
||||||
|
@import "tailwindcss/components";
|
||||||
|
@import "./component/common";
|
||||||
|
@import "./component/alert";
|
||||||
|
@import "./component/badge";
|
||||||
|
@import "./component/button";
|
||||||
|
@import "./component/card";
|
||||||
|
@import "./component/collapse";
|
||||||
|
@import "./component/dropdown";
|
||||||
|
@import "./component/modal";
|
||||||
|
@import "./component/progress";
|
||||||
|
@import "./component/tab";
|
||||||
|
@import "./component/table";
|
||||||
|
|
||||||
|
/* Form Import Tailwind CSS */
|
||||||
|
@import "./form/global";
|
||||||
|
@import "./form/text";
|
||||||
|
@import "./form/textarea";
|
||||||
|
@import "./form/box";
|
||||||
|
@import "./form/button";
|
||||||
|
@import "./form/otp";
|
||||||
|
@import "./form/color";
|
||||||
|
@import "./form/file";
|
||||||
|
@import "./form/range";
|
||||||
|
@import "./form/dropzone";
|
||||||
|
|
||||||
|
/* Utilities Import Tailwind CSS */
|
||||||
|
@import "tailwindcss/utilities";
|
||||||
|
@import "./utility/typography";
|
||||||
29
assets/style/css/utility/typography.css
Normal file
29
assets/style/css/utility/typography.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
h1 {
|
||||||
|
@apply text-4xl
|
||||||
|
font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
@apply text-3xl
|
||||||
|
font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
@apply text-2xl
|
||||||
|
font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
@apply text-xl
|
||||||
|
font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
@apply text-lg
|
||||||
|
font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
@apply text-base
|
||||||
|
font-bold;
|
||||||
|
}
|
||||||
49
assets/style/scss/custom/apps/products.scss
Normal file
49
assets/style/scss/custom/apps/products.scss
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
$small: 640px;
|
||||||
|
$medium: 768px;
|
||||||
|
$large: 1024px;
|
||||||
|
$xlarge: 1280px;
|
||||||
|
|
||||||
|
.filter-wrapper {
|
||||||
|
&.filter-wrapper-show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.filter-wrapper-hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $medium) {
|
||||||
|
.filter-wrapper {
|
||||||
|
transition: right 0.25s ease;
|
||||||
|
&.filter-wrapper-show {
|
||||||
|
display: block;
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.filter-wrapper-hide {
|
||||||
|
display: block;
|
||||||
|
right: -260px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: -50;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hide {
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
assets/style/scss/custom/layout/horizontal.scss
Normal file
18
assets/style/scss/custom/layout/horizontal.scss
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.h-layout {
|
||||||
|
.content-page {
|
||||||
|
padding: 10.5rem 2rem 0 2rem;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-header,
|
||||||
|
.w-header-search {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-header-search {
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
}
|
||||||
166
assets/style/scss/custom/layout/vertical.scss
Normal file
166
assets/style/scss/custom/layout/vertical.scss
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
$small: 640px;
|
||||||
|
$medium: 768px;
|
||||||
|
$large: 1024px;
|
||||||
|
$xlarge: 1280px;
|
||||||
|
|
||||||
|
@media screen and (max-width: $medium) {
|
||||||
|
body:has(.menu-overlay:not(.hide)) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
height: 120vh;
|
||||||
|
z-index: 45;
|
||||||
|
display: block;
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
background-color: rgba(34, 41, 47, 0.5);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-overlay {
|
||||||
|
&.hide {
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout Vertical CSS
|
||||||
|
.v-layout {
|
||||||
|
.content-page {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-header-search {
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-header,
|
||||||
|
.w-header-search {
|
||||||
|
width: calc(100% - 256px);
|
||||||
|
|
||||||
|
@media screen and (max-width: $medium) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-menu {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-page {
|
||||||
|
margin-left: 256px;
|
||||||
|
padding: 6rem 2rem 10px 2rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: $medium) {
|
||||||
|
margin-left: 0px;
|
||||||
|
padding: 6rem 1.25rem 0 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.menu-hide {
|
||||||
|
.w-header {
|
||||||
|
width: calc(100% - 0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-menu {
|
||||||
|
left: -260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-page {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content,
|
||||||
|
.menu-content-child {
|
||||||
|
max-height: 1000px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.hide {
|
||||||
|
max-height: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content-max,
|
||||||
|
.menu-content-child-max {
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.side-menu-arrow {
|
||||||
|
transition: 0.25s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-open {
|
||||||
|
svg.side-menu-arrow {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom styles for LZS theme
|
||||||
|
html[data-theme="LZS"] {
|
||||||
|
.v-layout {
|
||||||
|
.active-menu {
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
// Yellow glow on active menu items
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 4px;
|
||||||
|
background-color: rgb(var(--active-menu-border));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon color for active menu
|
||||||
|
svg {
|
||||||
|
color: rgb(var(--active-menu-border));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form focus states with yellow accent
|
||||||
|
input:focus, textarea:focus, select:focus {
|
||||||
|
outline: 2px solid rgba(var(--focus-ring));
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button hover with yellow accent
|
||||||
|
.btn-primary:hover, button.bg-primary:hover {
|
||||||
|
box-shadow: 0 0 0 2px rgba(var(--active-menu-border), 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card headers with subtle yellow accent
|
||||||
|
.rs-card {
|
||||||
|
.card-header {
|
||||||
|
border-bottom: 1px solid rgba(var(--border-color));
|
||||||
|
|
||||||
|
h2, h3, h4 {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: -10px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
height: 60%;
|
||||||
|
width: 3px;
|
||||||
|
background-color: rgb(var(--active-menu-border));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
assets/style/scss/custom/library/_dropdown.scss
Normal file
27
assets/style/scss/custom/library/_dropdown.scss
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
$small: 640px;
|
||||||
|
$medium: 768px;
|
||||||
|
$large: 1024px;
|
||||||
|
$xlarge: 1280px;
|
||||||
|
|
||||||
|
.s-dropdown {
|
||||||
|
background-color: white;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
top: 65px !important;
|
||||||
|
border: 1px solid rgb(229, 231, 235);
|
||||||
|
box-shadow: 0px 0px 10px rgba(226, 232, 240, 0.2);
|
||||||
|
|
||||||
|
@media screen and (max-width: $medium) {
|
||||||
|
left: 0px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
margin: 0px 2px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.s-dropdown {
|
||||||
|
background-color: #1e293b;
|
||||||
|
box-shadow: 0px 0px 10px rgba(15, 23, 42, 0.2);
|
||||||
|
border: 1px solid #182130;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
assets/style/scss/custom/library/_floatingvue.scss
Normal file
19
assets/style/scss/custom/library/_floatingvue.scss
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.v-popper--theme-dropdown .v-popper__inner {
|
||||||
|
background: rgb(var(--bg-2)) !important;
|
||||||
|
border: 0px !important;
|
||||||
|
color: rgb(var(--text-color)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable arrow
|
||||||
|
.v-popper__arrow-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// .v-popper--theme-my-theme .v-popper__inner {
|
||||||
|
// background: #fff000;
|
||||||
|
// color: black;
|
||||||
|
// padding: 24px;
|
||||||
|
// border-radius: 6px;
|
||||||
|
// border: 1px solid #ddd;
|
||||||
|
// box-shadow: 0 6px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
// }
|
||||||
237
assets/style/scss/custom/library/_formkit.scss
Normal file
237
assets/style/scss/custom/library/_formkit.scss
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
$small: 640px;
|
||||||
|
$medium: 768px;
|
||||||
|
$large: 1024px;
|
||||||
|
$xlarge: 1280px;
|
||||||
|
|
||||||
|
.formkit-form {
|
||||||
|
.form-wizard {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.top-form {
|
||||||
|
.formkit-actions {
|
||||||
|
button[type="submit"] {
|
||||||
|
display: block;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ul.top-steps {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: $medium) {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
position: relative;
|
||||||
|
bottom: -8px;
|
||||||
|
width: 0%;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: rgb(var(--color-primary));
|
||||||
|
transition: width 0.25s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step--errors {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
float: right;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #f23131;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: rgb(var(--bg-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-step-active="true"] {
|
||||||
|
color: rgb(var(--color-primary));
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-step-active="false"] {
|
||||||
|
color: rgb(var(--bg-1));
|
||||||
|
.progress {
|
||||||
|
width: 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-step-completed="true"] {
|
||||||
|
color: rgb(var(--color-primary));
|
||||||
|
font-weight: 600;
|
||||||
|
.progress {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.left-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: 230px auto;
|
||||||
|
|
||||||
|
@media screen and (max-width: $medium) {
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
gap: 0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-actions {
|
||||||
|
grid-column: span 2 / span 2;
|
||||||
|
|
||||||
|
@media screen and (max-width: $medium) {
|
||||||
|
grid-column: span 1 / span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[type="submit"] {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.left-steps {
|
||||||
|
display: block;
|
||||||
|
li {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
.progress {
|
||||||
|
position: relative;
|
||||||
|
bottom: -8px;
|
||||||
|
width: 0%;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: rgb(var(--color-primary));
|
||||||
|
transition: width 0.25s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step--errors {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
float: right;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #f23131;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: rgb(var(--bg-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-step-active="true"] {
|
||||||
|
color: rgb(var(--color-primary));
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-step-active="false"] {
|
||||||
|
color: rgb(var(--bg-1));
|
||||||
|
.progress {
|
||||||
|
width: 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-step-completed="true"] {
|
||||||
|
color: rgb(var(--color-primary));
|
||||||
|
font-weight: 600;
|
||||||
|
.progress {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom Formkit Input for VSelect
|
||||||
|
.formkit-vselect {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
.vs__dropdown-menu {
|
||||||
|
background: rgb(var(--bg-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs__dropdown-menu li {
|
||||||
|
background: rgb(var(--bg-1));
|
||||||
|
color: rgb(var(--text-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs__dropdown-toggle {
|
||||||
|
min-height: 2.5rem;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
color: rgb(var(--color-primary));
|
||||||
|
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
border-color: rgb(var(--fk-border-color));
|
||||||
|
|
||||||
|
.vs__selected {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
|
||||||
|
background-color: rgb(var(--color-primary));
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-right: 0.35rem;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom Decorator
|
||||||
|
input[type="checkbox"]:checked.icon-check + .formkit-decorator::before {
|
||||||
|
content: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>');
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
114
assets/style/scss/custom/library/_fullcalendar.scss
Normal file
114
assets/style/scss/custom/library/_fullcalendar.scss
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
$small: 640px;
|
||||||
|
$medium: 768px;
|
||||||
|
$large: 1024px;
|
||||||
|
$xlarge: 1280px;
|
||||||
|
|
||||||
|
.fc-theme-standard td,
|
||||||
|
.fc-theme-standard th {
|
||||||
|
border-color: rgb(var(--bg-1)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite fullcalendar styles light
|
||||||
|
.fc {
|
||||||
|
.fc-scrollgrid {
|
||||||
|
border: 1px solid rgb(var(--bg-1)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-toolbar {
|
||||||
|
@media screen and (max-width: $medium) {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-toolbar-chunk {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-button {
|
||||||
|
padding: 0.75rem 1rem !important;
|
||||||
|
|
||||||
|
&.fc-button-primary {
|
||||||
|
background-color: transparent !important;
|
||||||
|
color: rgb(var(--color-primary));
|
||||||
|
border-color: rgb(var(--color-primary)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fc-button-active {
|
||||||
|
background-color: rgb(var(--color-primary)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgb(var(--color-primary)) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 0.05rem rgba(255, 113, 133, 0.5) !important;
|
||||||
|
background-color: rgb(var(--color-primary)) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-view-harness {
|
||||||
|
table {
|
||||||
|
&.fc-col-header {
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-color: rgb(var(--bg-1)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-col-header-cell {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-day {
|
||||||
|
&.fc-day-today {
|
||||||
|
background-color: rgb(var(--bg-1));
|
||||||
|
background: rgb(var(--bg-1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-day-top {
|
||||||
|
color: #6b727f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-more-link {
|
||||||
|
color: rgb(var(--color-primary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-event-harness {
|
||||||
|
.fc-daygrid-event {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-h-event {
|
||||||
|
&.fc-event-start {
|
||||||
|
border-color: rgb(var(--color-primary));
|
||||||
|
background-color: rgb(var(--color-primary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fc-list {
|
||||||
|
.fc-list-event-dot {
|
||||||
|
background-color: rgb(var(--color-primary));
|
||||||
|
border: 5px solid #f3f4f6;
|
||||||
|
border-color: rgb(var(--color-primary)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-list-day-cushion {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-popover-header {
|
||||||
|
padding: 0.5rem !important;
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
81
assets/style/scss/custom/library/_nprogress.scss
Normal file
81
assets/style/scss/custom/library/_nprogress.scss
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/* Make clicks pass-through */
|
||||||
|
#nprogress {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress .bar {
|
||||||
|
background: #fb7185;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1031;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fancy blur effect */
|
||||||
|
#nprogress .peg {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100%;
|
||||||
|
box-shadow: 0 0 10px #fb7185, 0 0 5px #fb7185;
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
-webkit-transform: rotate(3deg) translate(0px, -4px);
|
||||||
|
-ms-transform: rotate(3deg) translate(0px, -4px);
|
||||||
|
transform: rotate(3deg) translate(0px, -4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove these to get rid of the spinner */
|
||||||
|
#nprogress .spinner {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1031;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress .spinner-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
border: solid 2px transparent;
|
||||||
|
border-top-color: #fb7185;
|
||||||
|
border-left-color: #fb7185;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
-webkit-animation: nprogress-spinner 400ms linear infinite;
|
||||||
|
animation: nprogress-spinner 400ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nprogress-custom-parent {
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nprogress-custom-parent #nprogress .spinner,
|
||||||
|
.nprogress-custom-parent #nprogress .bar {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes nprogress-spinner {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes nprogress-spinner {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
assets/style/scss/custom/library/_swiper.scss
Normal file
18
assets/style/scss/custom/library/_swiper.scss
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.swiper-button-next,
|
||||||
|
.swiper-button-prev {
|
||||||
|
color: #4B5563 !important;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-button-next:after,
|
||||||
|
.swiper-button-prev:after {
|
||||||
|
font-size: 1.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-pagination-bullet {
|
||||||
|
background-color: #8b98a9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-pagination-bullet-active {
|
||||||
|
background: #4B5563 !important;
|
||||||
|
}
|
||||||
7
assets/style/scss/custom/library/_vuecountryflag.scss
Normal file
7
assets/style/scss/custom/library/_vuecountryflag.scss
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.flag.normal-flag {
|
||||||
|
margin: 0em -1em -0em -1em !important;
|
||||||
|
transform: scale(0.38) !important;
|
||||||
|
-ms-transform: scale(0.38) !important;
|
||||||
|
-webkit-transform: scale(0.38) !important;
|
||||||
|
-moz-transform: scale(0.38) !important;
|
||||||
|
}
|
||||||
3
assets/style/scss/custom/library/_vuetoastification.scss
Normal file
3
assets/style/scss/custom/library/_vuetoastification.scss
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.Vue-Toastification__toast {
|
||||||
|
padding: 18px 21px;
|
||||||
|
}
|
||||||
21
assets/style/scss/custom/scrollbar/scrollbar.scss
Normal file
21
assets/style/scss/custom/scrollbar/scrollbar.scss
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/* width */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Track */
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle */
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgb(var(--scroll-color));
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle on hover */
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgb(var(--scroll-hover-color));
|
||||||
|
}
|
||||||
12
assets/style/scss/custom/transition/fade-up.scss
Normal file
12
assets/style/scss/custom/transition/fade-up.scss
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Animation for transition fade
|
||||||
|
.fade-up-enter-active,
|
||||||
|
.fade-up-leave-active {
|
||||||
|
transition: transform 0.4s cubic-bezier(0.17, 0.67, 0.84, 0.79),
|
||||||
|
opacity 0.3s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-up-enter-from,
|
||||||
|
.fade-up-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10%);
|
||||||
|
}
|
||||||
10
assets/style/scss/custom/transition/fade.scss
Normal file
10
assets/style/scss/custom/transition/fade.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// Animation for transition fade
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
11
assets/style/scss/custom/transition/page.scss
Normal file
11
assets/style/scss/custom/transition/page.scss
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Animation for transition fade
|
||||||
|
.page-enter-active,
|
||||||
|
.page-leave-active {
|
||||||
|
transition: transform 0.3s ease, opacity 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-enter-from,
|
||||||
|
.page-leave-to {
|
||||||
|
transform: scale(1.02);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
17
assets/style/scss/custom/transition/slide-fade.scss
Normal file
17
assets/style/scss/custom/transition/slide-fade.scss
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
Enter and leave animations can use different
|
||||||
|
durations and timing functions.
|
||||||
|
*/
|
||||||
|
.slide-fade-enter-active {
|
||||||
|
transition: all 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-leave-active {
|
||||||
|
transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-enter-from,
|
||||||
|
.slide-fade-leave-to {
|
||||||
|
transform: translateX(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
10
assets/style/scss/custom/transition/slide.scss
Normal file
10
assets/style/scss/custom/transition/slide.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.slide-enter-active,
|
||||||
|
.slide-leave-active {
|
||||||
|
transition: translateY, opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-enter-from,
|
||||||
|
.slide-leave-to {
|
||||||
|
transform: translateY(0px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
57
assets/style/scss/main.scss
Normal file
57
assets/style/scss/main.scss
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*================================================================================
|
||||||
|
Notes: This file is the main entry point for the SCSS stylesheet.
|
||||||
|
================================================================================*/
|
||||||
|
|
||||||
|
/* Import DM Sans font from Google Fonts */
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap");
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#__nuxt {
|
||||||
|
font-family: "DM Sans", sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -0.5px; /* Changed from -2px to -0.5px for better readability */
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// svg.icon{
|
||||||
|
// width: 1.3rem;
|
||||||
|
// height: 1.3rem;
|
||||||
|
// vertical-align: middle;
|
||||||
|
// fill: currentColor;
|
||||||
|
// overflow: hidden;
|
||||||
|
// }
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: rgb(233, 74, 74);
|
||||||
|
background-color: rgba(146, 146, 146, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom Layout SCSS
|
||||||
|
@import "./custom/layout/vertical";
|
||||||
|
@import "./custom/layout/horizontal";
|
||||||
|
|
||||||
|
// Transition SCSS
|
||||||
|
@import "./custom/transition/page";
|
||||||
|
@import "./custom/transition/fade";
|
||||||
|
@import "./custom/transition/fade-up";
|
||||||
|
@import "./custom/transition/slide";
|
||||||
|
@import "./custom/transition/slide-fade";
|
||||||
|
|
||||||
|
// Scrollbar SCSS
|
||||||
|
@import "./custom/scrollbar/scrollbar";
|
||||||
|
|
||||||
|
// Custom SCSS library
|
||||||
|
@import "./custom/library/dropdown";
|
||||||
|
@import "./custom/library/nprogress";
|
||||||
|
@import "./custom/library/formkit";
|
||||||
|
@import "./custom/library/vuetoastification";
|
||||||
|
@import "./custom/library/swiper";
|
||||||
|
@import "./custom/library/fullcalendar";
|
||||||
|
@import "./custom/library/floatingvue";
|
||||||
|
@import "./custom/library/vuecountryflag";
|
||||||
78
components/FontSizeStepper.vue
Normal file
78
components/FontSizeStepper.vue
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Number,
|
||||||
|
default: 16
|
||||||
|
},
|
||||||
|
min: {
|
||||||
|
type: Number,
|
||||||
|
default: 12
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: Number,
|
||||||
|
default: 32
|
||||||
|
},
|
||||||
|
step: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const increment = () => {
|
||||||
|
if (props.modelValue < props.max && !props.disabled) {
|
||||||
|
emit('update:modelValue', props.modelValue + props.step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const decrement = () => {
|
||||||
|
if (props.modelValue > props.min && !props.disabled) {
|
||||||
|
emit('update:modelValue', props.modelValue - props.step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInput = (event) => {
|
||||||
|
const value = parseInt(event.target.value) || props.min
|
||||||
|
const clampedValue = Math.max(props.min, Math.min(props.max, value))
|
||||||
|
emit('update:modelValue', clampedValue)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
@click="decrement"
|
||||||
|
:disabled="modelValue <= min || disabled"
|
||||||
|
class="w-8 h-8 flex items-center justify-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Icon name="ic:round-remove" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
:value="modelValue"
|
||||||
|
@input="handleInput"
|
||||||
|
:disabled="disabled"
|
||||||
|
type="number"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
class="w-16 px-2 py-1 text-center border border-gray-300 dark:border-gray-600 rounded focus:border-primary dark:bg-gray-700 dark:text-white"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="increment"
|
||||||
|
:disabled="modelValue >= max || disabled"
|
||||||
|
class="w-8 h-8 flex items-center justify-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Icon name="ic:round-add" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span class="text-sm text-gray-500">px</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
105
components/Loading.vue
Normal file
105
components/Loading.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<script setup>
|
||||||
|
const { siteSettings, loading: siteSettingsLoading } = useSiteSettings();
|
||||||
|
|
||||||
|
const showMessage = ref(false);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
showMessage.value = true;
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
const refreshPage = () => {
|
||||||
|
// hard refresh
|
||||||
|
window.location.reload(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fast loading logo - fetch during SSR to prevent hydration flash
|
||||||
|
const { data: quickLoadingData } = await useLazyFetch(
|
||||||
|
"/api/devtool/config/loading-logo",
|
||||||
|
{
|
||||||
|
default: () => ({
|
||||||
|
data: {
|
||||||
|
siteLoadingLogo: "",
|
||||||
|
siteName: "Loading...",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
transform: (response) =>
|
||||||
|
response.data || {
|
||||||
|
siteLoadingLogo: "",
|
||||||
|
siteName: "Loading...",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadingLogoSrc = computed(() => {
|
||||||
|
// First priority: Quick loading data if available
|
||||||
|
if (quickLoadingData.value?.siteLoadingLogo) {
|
||||||
|
return quickLoadingData.value.siteLoadingLogo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second priority: Full site settings if loaded
|
||||||
|
if (!siteSettingsLoading.value && siteSettings.value.siteLoadingLogo) {
|
||||||
|
return siteSettings.value.siteLoadingLogo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Default logo
|
||||||
|
return "/img/logo/corradAF-logo.svg";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get site name with fallback
|
||||||
|
const getSiteName = () => {
|
||||||
|
// First priority: Quick loading data
|
||||||
|
if (quickLoadingData.value?.siteName) {
|
||||||
|
return quickLoadingData.value.siteName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second priority: Full site settings
|
||||||
|
if (!siteSettingsLoading.value && siteSettings.value.siteName) {
|
||||||
|
return siteSettings.value.siteName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
return "Loading...";
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rs-loading bg-white absolute z-50 top-0 left-0 w-full h-full">
|
||||||
|
<div class="flex justify-center text-center items-center h-screen">
|
||||||
|
<div>
|
||||||
|
<div class="img-container flex justify-center items-center mb-5">
|
||||||
|
<!-- Use custom loading logo if available, otherwise show single default logo -->
|
||||||
|
<img
|
||||||
|
src="@/assets/img/logo/lzs-logo.png"
|
||||||
|
class="max-w-[180px] max-h-[60px] object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex justify-center items-center"
|
||||||
|
aria-label="Loading..."
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<svg class="h-14 w-14 animate-spin" viewBox="3 3 18 18">
|
||||||
|
<path
|
||||||
|
class="fill-[#00A59A]/10"
|
||||||
|
d="M12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5ZM3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="fill-[#00A59A]"
|
||||||
|
d="M16.9497 7.05015C14.2161 4.31648 9.78392 4.31648 7.05025 7.05015C6.65973 7.44067 6.02656 7.44067 5.63604 7.05015C5.24551 6.65962 5.24551 6.02646 5.63604 5.63593C9.15076 2.12121 14.8492 2.12121 18.364 5.63593C18.7545 6.02646 18.7545 6.65962 18.364 7.05015C17.9734 7.44067 17.3403 7.44067 16.9497 7.05015Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div v-if="showMessage" class="my-10 text-gray-500 font-medium">
|
||||||
|
If loading takes too long,
|
||||||
|
<br />
|
||||||
|
please click
|
||||||
|
<button @click="refreshPage">
|
||||||
|
<span class="text-[#F3586A]">here</span>
|
||||||
|
</button>
|
||||||
|
or hard refresh your browser.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
243
components/RSCalendar.vue
Normal file
243
components/RSCalendar.vue
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
<script setup>
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
events: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dateNow = ref(DateTime.now());
|
||||||
|
const arrDate = ref([]);
|
||||||
|
|
||||||
|
const dayInWeek = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||||
|
|
||||||
|
const monthInYear = [
|
||||||
|
"January",
|
||||||
|
"February",
|
||||||
|
"March",
|
||||||
|
"April",
|
||||||
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"October",
|
||||||
|
"November",
|
||||||
|
"December",
|
||||||
|
];
|
||||||
|
|
||||||
|
const label = (date) => {
|
||||||
|
// console.log(date.toString());
|
||||||
|
return date.toFormat("d");
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextMonth = () => {
|
||||||
|
dateNow.value = DateTime.local(
|
||||||
|
dateNow.value.year,
|
||||||
|
dateNow.value.month,
|
||||||
|
dateNow.value.day
|
||||||
|
).plus({
|
||||||
|
months: 1,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevMonth = () => {
|
||||||
|
dateNow.value = DateTime.local(
|
||||||
|
dateNow.value.year,
|
||||||
|
dateNow.value.month,
|
||||||
|
dateNow.value.day
|
||||||
|
).minus({
|
||||||
|
months: 1,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDateInMonth = () => {
|
||||||
|
const date = DateTime.local(dateNow.value.year, dateNow.value.month);
|
||||||
|
const dayInMonth = date.daysInMonth;
|
||||||
|
|
||||||
|
arrDate.value = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < dayInMonth; i++) {
|
||||||
|
const day = i + 1;
|
||||||
|
const date = DateTime.local(dateNow.value.year, dateNow.value.month, day);
|
||||||
|
const toDay = date.toFormat("ccc");
|
||||||
|
// Start of the Date
|
||||||
|
if (i == 0) {
|
||||||
|
const toDayIndex = dayInWeek.indexOf(toDay);
|
||||||
|
|
||||||
|
for (let i = 0; i < toDayIndex; i++) {
|
||||||
|
const dayBefore = toDayIndex - i;
|
||||||
|
const dateBefore = date.minus({ days: dayBefore });
|
||||||
|
|
||||||
|
updateArrDate(dateBefore, false, false);
|
||||||
|
}
|
||||||
|
if (DateTime.now().toISODate() == date.toISODate())
|
||||||
|
updateArrDate(date, true, true);
|
||||||
|
else updateArrDate(date, true, false);
|
||||||
|
}
|
||||||
|
// End of the Date
|
||||||
|
else if (i == dayInMonth - 1) {
|
||||||
|
const toDayIndex = dayInWeek.length - dayInWeek.indexOf(toDay) - 1;
|
||||||
|
|
||||||
|
if (DateTime.now().toISODate() == date.toISODate())
|
||||||
|
updateArrDate(date, true, true);
|
||||||
|
else updateArrDate(date, true, false);
|
||||||
|
|
||||||
|
for (let i = 0; i < toDayIndex; i++) {
|
||||||
|
const dayAfter = i + 1;
|
||||||
|
const dateAfter = date.plus({ days: dayAfter });
|
||||||
|
|
||||||
|
updateArrDate(dateAfter, false, false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (DateTime.now().toISODate() == date.toISODate())
|
||||||
|
updateArrDate(date, true, true);
|
||||||
|
else updateArrDate(date, true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(arrDate.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateArrDate = (date, currentMonth, isToday) => {
|
||||||
|
arrDate.value.push({
|
||||||
|
date: date,
|
||||||
|
isCurrentMonth: currentMonth,
|
||||||
|
isToday: isToday,
|
||||||
|
event: checkEvent(date),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check props.event start and end date with date given
|
||||||
|
const checkEvent = (date) => {
|
||||||
|
let result = false;
|
||||||
|
props.events.forEach((event) => {
|
||||||
|
if (event.startDate) {
|
||||||
|
let startDate = "";
|
||||||
|
let endDate = "";
|
||||||
|
startDate = DateTime.fromISO(event.startDate);
|
||||||
|
if (event.endDate) {
|
||||||
|
endDate = DateTime.fromISO(event.endDate);
|
||||||
|
}
|
||||||
|
if (date.toISODate() == startDate.toISODate())
|
||||||
|
result = [
|
||||||
|
{
|
||||||
|
position: "start",
|
||||||
|
title: event.title,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
else if (
|
||||||
|
date.toISODate() > startDate.toISODate() &&
|
||||||
|
date.toISODate() < endDate.toISODate()
|
||||||
|
)
|
||||||
|
result = [
|
||||||
|
{
|
||||||
|
position: "between",
|
||||||
|
title: event.title,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
else if (date.toISODate() == endDate.toISODate())
|
||||||
|
result = [
|
||||||
|
{
|
||||||
|
position: "end",
|
||||||
|
title: event.title,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
else result = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result; // return result
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getDateInMonth();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(dateNow, () => {
|
||||||
|
getDateInMonth();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="calendar">
|
||||||
|
<div class="calendar-header">
|
||||||
|
<!-- <div class="flex">1</div> -->
|
||||||
|
<div class="flex justify-between items-center my-4">
|
||||||
|
<h5>
|
||||||
|
{{ date.toFormat("LLLL yyyy") }}
|
||||||
|
</h5>
|
||||||
|
<div class="flex gap-5">
|
||||||
|
<button
|
||||||
|
@click="prevMonth"
|
||||||
|
class="flex items-center px-2 py-2 rounded-md shadow-md bg-white text-primary hover:bg-primary/80 hover:text-white"
|
||||||
|
>
|
||||||
|
<Icon size="20px" name="ic:round-keyboard-arrow-left"></Icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="nextMonth"
|
||||||
|
class="flex items-center px-2 py-2 rounded-md shadow-md bg-white text-primary hover:bg-primary/80 hover:text-white"
|
||||||
|
>
|
||||||
|
<Icon size="20px" name="ic:round-keyboard-arrow-right"></Icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-body rounded-md border border-primary/20">
|
||||||
|
<div class="calendar-body-header max-w-full">
|
||||||
|
<ul
|
||||||
|
class="grid grid-cols-7 list-none bg-primary/50 text-primary rounded-t-md"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="flex justify-center items-center p-5"
|
||||||
|
v-for="(val, index) in dayInWeek"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<span class="font-semibold text-base">{{ val }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-body-content">
|
||||||
|
<ul class="grid grid-cols-7 list-none">
|
||||||
|
<li
|
||||||
|
class="relative flex justify-center items-center h-30 border border-primary/10 whitespace-nowrap"
|
||||||
|
:class="{
|
||||||
|
'bg-primary/5': val.isToday,
|
||||||
|
}"
|
||||||
|
v-for="(val, index) in allDate"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<div class="flex-1">
|
||||||
|
<label
|
||||||
|
class="absolute top-2 right-3 font-semibold"
|
||||||
|
:class="{
|
||||||
|
'text-primary/20': !val.isCurrentMonth,
|
||||||
|
'text-primary/70': val.isCurrentMonth,
|
||||||
|
}"
|
||||||
|
for="day"
|
||||||
|
>{{ label(val.date) }}</label
|
||||||
|
>
|
||||||
|
<div class="event" v-if="val.event">
|
||||||
|
<div
|
||||||
|
class="font-semibold p-5 bg-primary text-white rounded-md"
|
||||||
|
style="min-height: 5rem"
|
||||||
|
:class="{
|
||||||
|
'rounded-r-none ml-5': event.position === 'start',
|
||||||
|
'rounded-r-none rounded-l-none':
|
||||||
|
event.position === 'between',
|
||||||
|
'rounded-l-none mr-5': event.position === 'end',
|
||||||
|
}"
|
||||||
|
v-for="(event, index) in val.event"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
{{ event.position == "start" ? event.title : " " }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
67
components/RsAlert.vue
Normal file
67
components/RsAlert.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: "primary",
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
dismissible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
autoDismiss: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
timer: {
|
||||||
|
type: Number,
|
||||||
|
default: 1000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const showComponent = ref(props.show);
|
||||||
|
|
||||||
|
const autoDismiss = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
showComponent.value = false;
|
||||||
|
}, props.timer);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.autoDismiss) {
|
||||||
|
autoDismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Transition name="fade-up">
|
||||||
|
<div
|
||||||
|
v-if="showComponent"
|
||||||
|
class="alert"
|
||||||
|
:class="{
|
||||||
|
'alert-primary': variant === 'primary',
|
||||||
|
'alert-secondary': variant === 'secondary',
|
||||||
|
'alert-info': variant === 'info',
|
||||||
|
'alert-success': variant === 'success',
|
||||||
|
'alert-warning': variant === 'warning',
|
||||||
|
'alert-danger': variant === 'danger',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Icon v-if="icon" :name="icon" />
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<button @click="showComponent = false">
|
||||||
|
<Icon name="ic:baseline-close" size="14"></Icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
145
components/RsApiTester.vue
Normal file
145
components/RsApiTester.vue
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = ref(window.location.origin + props.url);
|
||||||
|
const params = ref([
|
||||||
|
{
|
||||||
|
key: "",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const method = ref("POST");
|
||||||
|
const dropdownMethods = ref([
|
||||||
|
{
|
||||||
|
label: "GET",
|
||||||
|
value: "GET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "POST",
|
||||||
|
value: "POST",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "PUT",
|
||||||
|
value: "PUT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "DELETE",
|
||||||
|
value: "DELETE",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const bodyJson = ref("{}");
|
||||||
|
|
||||||
|
const response = ref("");
|
||||||
|
|
||||||
|
watch(
|
||||||
|
params,
|
||||||
|
() => {
|
||||||
|
const urlParams = new URLSearchParams();
|
||||||
|
params.value.forEach((param) => {
|
||||||
|
if (param.key !== "") {
|
||||||
|
urlParams.append(param.key, param.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
url.value = window.location.origin + props.url + "?" + urlParams.toString();
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const addParam = () => {
|
||||||
|
params.value.push({
|
||||||
|
key: "",
|
||||||
|
value: "",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeParam = (index) => {
|
||||||
|
if (params.value.length === 1) return;
|
||||||
|
params.value.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hitAPI = async () => {
|
||||||
|
const res = await useFetch(url.value, {
|
||||||
|
method: method.value,
|
||||||
|
initialCache: false,
|
||||||
|
body: JSON.stringify(bodyJson.value),
|
||||||
|
});
|
||||||
|
|
||||||
|
response.value = JSON.stringify(res.data.value, null, 2);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<FormKit type="text" label="URL" v-model="url" disabled />
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Method"
|
||||||
|
:options="dropdownMethods"
|
||||||
|
v-model="method"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label
|
||||||
|
class="formkit-label text-gray-700 dark:text-gray-200 mb-2 font-semibold text-sm formkit-invalid:text-red-500 dark:formkit-invalid:text-danger flex justify-between items-center"
|
||||||
|
for="input_8"
|
||||||
|
>Parameter
|
||||||
|
<rs-button size="sm" @click="addParam()"> Add</rs-button>
|
||||||
|
</label>
|
||||||
|
<div class="dynamic-params grid grid-cols-3 gap-4">
|
||||||
|
<div v-for="(val, index) in params">
|
||||||
|
<FormKit type="text" placeholder="Key" v-model="val.key" />
|
||||||
|
<FormKit type="text" placeholder="Value" v-model="val.value" />
|
||||||
|
<rs-button @click="removeParam(index)"> Remove</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<label
|
||||||
|
class="formkit-label text-gray-700 dark:text-gray-200 mb-2 font-semibold text-sm formkit-invalid:text-red-500 dark:formkit-invalid:text-danger flex justify-between items-center"
|
||||||
|
for="input_8"
|
||||||
|
>Body
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<ClientOnly>
|
||||||
|
<rs-code-mirror v-model="bodyJson" mode="application/json" height="300px">
|
||||||
|
</rs-code-mirror>
|
||||||
|
</ClientOnly>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<label
|
||||||
|
class="formkit-label text-gray-700 dark:text-gray-200 mb-2 font-semibold text-sm formkit-invalid:text-red-500 dark:formkit-invalid:text-danger flex justify-between items-center"
|
||||||
|
for="input_8"
|
||||||
|
>Response
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- <ClientOnly>
|
||||||
|
<rs-code-mirror v-model="response" height="300px" readonly="nocursor">
|
||||||
|
</rs-code-mirror>
|
||||||
|
</ClientOnly> -->
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="textarea"
|
||||||
|
:classes="{
|
||||||
|
input: '!bg-[#272822] text-white dark:bg-gray-800 dark:text-gray-200',
|
||||||
|
}"
|
||||||
|
rows="10"
|
||||||
|
v-model="response"
|
||||||
|
disabled
|
||||||
|
></FormKit>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<rs-button @click="hitAPI"> Test</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
34
components/RsBadge.vue
Normal file
34
components/RsBadge.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: "primary",
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
iconSize: {
|
||||||
|
type: String,
|
||||||
|
default: "18",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="badge capitalize"
|
||||||
|
:class="{
|
||||||
|
'badge-primary': variant === 'primary',
|
||||||
|
'badge-secondary': variant === 'secondary',
|
||||||
|
'badge-info': variant === 'info',
|
||||||
|
'badge-success': variant === 'success',
|
||||||
|
'badge-warning': variant === 'warning',
|
||||||
|
'badge-danger': variant === 'danger',
|
||||||
|
'badge-disabled': variant === 'disabled',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Icon v-if="icon" :name="icon" :size="iconSize"></Icon>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
58
components/RsButton.vue
Normal file
58
components/RsButton.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: "fill",
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: "primary",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: "md",
|
||||||
|
},
|
||||||
|
btnType: {
|
||||||
|
type: String,
|
||||||
|
default: "button",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
:type="btnType"
|
||||||
|
:class="{
|
||||||
|
'button-sm': size === 'sm',
|
||||||
|
'button-md': size === 'md',
|
||||||
|
'button-lg': size === 'lg',
|
||||||
|
|
||||||
|
// Filled Button
|
||||||
|
'button-primary': variant === 'primary',
|
||||||
|
'button-secondary': variant === 'secondary',
|
||||||
|
'button-info': variant === 'info',
|
||||||
|
'button-success': variant === 'success',
|
||||||
|
'button-warning': variant === 'warning',
|
||||||
|
'button-danger': variant === 'danger',
|
||||||
|
|
||||||
|
// Outline Button
|
||||||
|
'outline-primary': variant === 'primary-outline',
|
||||||
|
'outline-secondary': variant === 'secondary-outline',
|
||||||
|
'outline-info': variant === 'info-outline',
|
||||||
|
'outline-success': variant === 'success-outline',
|
||||||
|
'outline-warning': variant === 'warning-outline',
|
||||||
|
'outline-danger': variant === 'danger-outline',
|
||||||
|
|
||||||
|
//Text Button
|
||||||
|
'texts-primary': variant === 'primary-text',
|
||||||
|
'texts-secondary': variant === 'secondary-text',
|
||||||
|
'texts-info': variant === 'info-text',
|
||||||
|
'texts-success': variant === 'success-text',
|
||||||
|
'texts-warning': variant === 'warning-text',
|
||||||
|
'texts-danger': variant === 'danger-text',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
16
components/RsCard.vue
Normal file
16
components/RsCard.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<header v-if="!!$slots.header" class="card-header">
|
||||||
|
<slot name="header" />
|
||||||
|
</header>
|
||||||
|
<main><slot></slot></main>
|
||||||
|
<div v-if="!!$slots.body" class="card-body">
|
||||||
|
<slot name="body" />
|
||||||
|
</div>
|
||||||
|
<footer v-if="!!$slots.footer" class="card-footer">
|
||||||
|
<slot name="footer" />
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
223
components/RsCodeMirror.vue
Normal file
223
components/RsCodeMirror.vue
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useThemeStore } from "~/stores/theme";
|
||||||
|
|
||||||
|
import { vue } from "@codemirror/lang-vue";
|
||||||
|
import { javascript } from "@codemirror/lang-javascript";
|
||||||
|
|
||||||
|
import { oneDark } from "@codemirror/theme-one-dark";
|
||||||
|
import { amy, ayuLight, barf, clouds, cobalt, dracula } from "thememirror";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: "vue",
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: "70vh",
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
default: "oneDark",
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
const editorTheme = ref(themeStore.codeTheme);
|
||||||
|
|
||||||
|
const dropdownThemes = ref([
|
||||||
|
{
|
||||||
|
label: "default",
|
||||||
|
value: "clouds",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "oneDark",
|
||||||
|
value: "oneDark",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "amy",
|
||||||
|
value: "amy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "ayu",
|
||||||
|
value: "ayuLight",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "barf",
|
||||||
|
value: "barf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "cobalt",
|
||||||
|
value: "cobalt",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "dracula",
|
||||||
|
value: "dracula",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const value = ref(props.modelValue);
|
||||||
|
const extensions = ref([]);
|
||||||
|
if (props.mode == "vue") {
|
||||||
|
extensions.value = [vue(), oneDark];
|
||||||
|
} else {
|
||||||
|
extensions.value = [javascript(), oneDark];
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalLines = ref(0);
|
||||||
|
const totalLength = ref(0);
|
||||||
|
|
||||||
|
// Codemirror EditorView instance ref
|
||||||
|
const view = shallowRef();
|
||||||
|
const handleReady = (payload) => {
|
||||||
|
view.value = payload.view;
|
||||||
|
totalLines.value = view.value.state.doc.lines;
|
||||||
|
totalLength.value = view.value.state.doc.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => editorTheme.value,
|
||||||
|
(themeVal) => {
|
||||||
|
// themeStore.setCodeTheme(newValue.value);
|
||||||
|
|
||||||
|
if (props.mode == "vue") {
|
||||||
|
extensions.value = [
|
||||||
|
vue(),
|
||||||
|
themeVal === "oneDark"
|
||||||
|
? oneDark
|
||||||
|
: themeVal === "amy"
|
||||||
|
? amy
|
||||||
|
: themeVal === "ayuLight"
|
||||||
|
? ayuLight
|
||||||
|
: themeVal === "barf"
|
||||||
|
? barf
|
||||||
|
: themeVal === "cobalt"
|
||||||
|
? cobalt
|
||||||
|
: themeVal === "dracula"
|
||||||
|
? dracula
|
||||||
|
: clouds,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
extensions.value = [
|
||||||
|
javascript(),
|
||||||
|
themeVal === "oneDark"
|
||||||
|
? oneDark
|
||||||
|
: themeVal === "amy"
|
||||||
|
? amy
|
||||||
|
: themeVal === "ayuLight"
|
||||||
|
? ayuLight
|
||||||
|
: themeVal === "barf"
|
||||||
|
? barf
|
||||||
|
: themeVal === "cobalt"
|
||||||
|
? cobalt
|
||||||
|
: themeVal === "dracula"
|
||||||
|
? dracula
|
||||||
|
: clouds,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Status is available at all times via Codemirror EditorView
|
||||||
|
const getCodemirrorStates = () => {
|
||||||
|
const state = view.value.state;
|
||||||
|
const ranges = state.selection.ranges;
|
||||||
|
const selected = ranges.reduce((r, range) => r + range.to - range.from, 0);
|
||||||
|
const cursor = ranges[0].anchor;
|
||||||
|
const length = state.doc.length;
|
||||||
|
const lines = state.doc.lines;
|
||||||
|
|
||||||
|
console.log("state", view.value.state);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChange = (value) => {
|
||||||
|
// console.log("onChange", value);
|
||||||
|
emits("update:modelValue", value);
|
||||||
|
totalLines.value = view.value.state.doc.lines;
|
||||||
|
totalLength.value = view.value.state.doc.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFocus = (value) => {
|
||||||
|
// console.log("onFocus", value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBlur = (value) => {
|
||||||
|
// console.log("onBlur", value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdate = (value) => {
|
||||||
|
// console.log("onUpdate", value);
|
||||||
|
};
|
||||||
|
|
||||||
|
function numberComma(x) {
|
||||||
|
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center gap-2 p-2 bg-[#282C34] text-[#abb2bf]"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
Theme:
|
||||||
|
<FormKit
|
||||||
|
v-model="editorTheme"
|
||||||
|
type="select"
|
||||||
|
placeholder="Select Themes"
|
||||||
|
:options="dropdownThemes"
|
||||||
|
:classes="{
|
||||||
|
input:
|
||||||
|
'!bg-[#282C34] !text-[#abb2bf] !border-[#abb2bf] hover:cursor-pointer h-6 w-[100px]',
|
||||||
|
inner: ' !rounded-none !mb-0',
|
||||||
|
outer: '!mb-0',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- <rs-button
|
||||||
|
class="!p-2"
|
||||||
|
variant="primary-outline"
|
||||||
|
@click="getCodemirrorStates"
|
||||||
|
>
|
||||||
|
Get state</rs-button
|
||||||
|
> -->
|
||||||
|
</div>
|
||||||
|
<client-only>
|
||||||
|
<CodeMirror
|
||||||
|
v-model="value"
|
||||||
|
placeholder="Code goes here..."
|
||||||
|
:style="{ height: height }"
|
||||||
|
:autofocus="true"
|
||||||
|
:indent-with-tab="true"
|
||||||
|
:tab-size="2"
|
||||||
|
:extensions="extensions"
|
||||||
|
:disabled="disabled"
|
||||||
|
@ready="handleReady"
|
||||||
|
@change="onChange($event)"
|
||||||
|
@focus="onFocus($event)"
|
||||||
|
@blur="onBlur($event)"
|
||||||
|
@update="onUpdate($event)"
|
||||||
|
/>
|
||||||
|
</client-only>
|
||||||
|
<div
|
||||||
|
class="footer flex justify-end items-center gap-2 p-2 bg-[#282C34] text-[#abb2bf]"
|
||||||
|
>
|
||||||
|
<span class="">Lines: {{ numberComma(totalLines) }}</span>
|
||||||
|
<span class="">Length: {{ numberComma(totalLength) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
24
components/RsCollapse.vue
Normal file
24
components/RsCollapse.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
accordion: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="accordion"
|
||||||
|
:class="{
|
||||||
|
'accordion-border': type === 'border',
|
||||||
|
}"
|
||||||
|
v-uid
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
94
components/RsCollapseItem.vue
Normal file
94
components/RsCollapseItem.vue
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const collapseGroup = ref(null);
|
||||||
|
const parentID = ref(null);
|
||||||
|
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
|
||||||
|
const isAccordion = instance.parent.props.accordion;
|
||||||
|
const type = ref(instance.parent.props.type);
|
||||||
|
const height = ref(0);
|
||||||
|
const maxHeight = ref(60);
|
||||||
|
|
||||||
|
//watch intance type
|
||||||
|
watch(
|
||||||
|
() => instance.parent.props.type,
|
||||||
|
(newValue) => {
|
||||||
|
type.value = newValue;
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
const parentElement = document.querySelector(`#${collapseGroup.value.id}`);
|
||||||
|
parentID.value = parentElement.parentElement.id;
|
||||||
|
|
||||||
|
const scrollHeight = parentElement.scrollHeight;
|
||||||
|
const targetOpenCollapse = parentElement.classList.contains(
|
||||||
|
"accordion-group--open"
|
||||||
|
);
|
||||||
|
const openCollapse = document.querySelector(
|
||||||
|
`#${parentID.value} .accordion-group--open`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isAccordion) {
|
||||||
|
if (openCollapse) {
|
||||||
|
const openCollapseHeader = document.querySelector(
|
||||||
|
`#${parentID.value} .accordion-group--open .accordion-header`
|
||||||
|
);
|
||||||
|
openCollapse.style.maxHeight = `${openCollapseHeader.scrollHeight}px`;
|
||||||
|
openCollapse.classList.remove("accordion-group--open");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetOpenCollapse) {
|
||||||
|
parentElement.style.maxHeight = maxHeight.value + "px";
|
||||||
|
parentElement.classList.remove("accordion-group--open");
|
||||||
|
} else {
|
||||||
|
parentElement.style.maxHeight = scrollHeight + "px";
|
||||||
|
parentElement.classList.add("accordion-group--open");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// On mounted get height collapse header
|
||||||
|
onMounted(() => {
|
||||||
|
try {
|
||||||
|
const parentElement = document.querySelector(
|
||||||
|
`#${collapseGroup.value.id} .accordion-header`
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrollHeight = parentElement.scrollHeight;
|
||||||
|
maxHeight.value = scrollHeight;
|
||||||
|
height.value = scrollHeight;
|
||||||
|
} catch (error) {
|
||||||
|
// console.log(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-uid
|
||||||
|
ref="collapseGroup"
|
||||||
|
class="accordion-group"
|
||||||
|
:class="{
|
||||||
|
'accordion-default': type === 'default',
|
||||||
|
'accordion-border': type === 'border',
|
||||||
|
'accordion-card': type === 'card',
|
||||||
|
}"
|
||||||
|
:style="`max-height: ${maxHeight}px; transition-property: max-height`"
|
||||||
|
>
|
||||||
|
<div class="accordion-header" @click="onClick">
|
||||||
|
<slot v-if="!!$slots.title" name="title"></slot>
|
||||||
|
<span v-else> {{ title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="accordion-body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
222
components/RsDropdown.vue
Normal file
222
components/RsDropdown.vue
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
<script setup>
|
||||||
|
import { directive as vClickAway } from "vue3-click-away";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: "Default",
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: "primary",
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: String,
|
||||||
|
default: "bottom",
|
||||||
|
},
|
||||||
|
textAlign: {
|
||||||
|
type: String,
|
||||||
|
default: "left",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: "md",
|
||||||
|
},
|
||||||
|
itemSize: {
|
||||||
|
type: String,
|
||||||
|
default: "10rem",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOpen = ref(false);
|
||||||
|
const dropdownRef = ref(null);
|
||||||
|
|
||||||
|
let originalPosition = null; // Store the original position of the dropdown
|
||||||
|
let lastKnownPosition = null; // Store the last known position of the dropdown
|
||||||
|
|
||||||
|
const toggle = (event) => {
|
||||||
|
isOpen.value = !isOpen.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeMenu = (event) => {
|
||||||
|
isOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a watcher for isOpen to reposition the dropdown when it's open
|
||||||
|
watch(isOpen, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
positionDropdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to position the dropdown relative to the viewport
|
||||||
|
const positionDropdown = () => {
|
||||||
|
const dropdownElement = dropdownRef.value;
|
||||||
|
const dropdownSection = dropdownElement.querySelector(".dropdown-section");
|
||||||
|
|
||||||
|
if (!dropdownElement || !dropdownSection) return;
|
||||||
|
|
||||||
|
// Get the bounding rect of the dropdown and its section
|
||||||
|
const dropdownRect = dropdownElement.getBoundingClientRect();
|
||||||
|
const dropdownSectionRect = dropdownSection.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Get the viewport dimensions
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
|
// Check if the dropdown overflows the right or left side of the viewport
|
||||||
|
const rightOverflow =
|
||||||
|
dropdownRect.right + dropdownSectionRect.width - viewportWidth;
|
||||||
|
const leftOverflow = dropdownRect.left - dropdownSectionRect.width;
|
||||||
|
|
||||||
|
if (rightOverflow > 0) {
|
||||||
|
dropdownSection.style.right = "0";
|
||||||
|
dropdownSection.style.left = "unset";
|
||||||
|
} else if (leftOverflow < 0) {
|
||||||
|
dropdownSection.style.left = "0";
|
||||||
|
dropdownSection.style.right = "unset";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the dropdown overflows the bottom or top of the viewport
|
||||||
|
const bottomOverflow =
|
||||||
|
dropdownRect.bottom + dropdownSectionRect.height - viewportHeight;
|
||||||
|
const topOverflow = dropdownRect.top - dropdownSectionRect.height;
|
||||||
|
|
||||||
|
if (bottomOverflow > 0) {
|
||||||
|
dropdownSection.style.bottom = "100%";
|
||||||
|
dropdownSection.style.top = "unset";
|
||||||
|
} else if (topOverflow < 0) {
|
||||||
|
dropdownSection.style.top = "100%";
|
||||||
|
dropdownSection.style.bottom = "unset";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the position changed and update the lastKnownPosition
|
||||||
|
const newPosition = dropdownSection.getBoundingClientRect();
|
||||||
|
if (
|
||||||
|
!lastKnownPosition ||
|
||||||
|
JSON.stringify(lastKnownPosition) !== JSON.stringify(newPosition)
|
||||||
|
) {
|
||||||
|
lastKnownPosition = newPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the dropdown is out of the viewport and reset its position to original
|
||||||
|
if (
|
||||||
|
isOpen.value &&
|
||||||
|
originalPosition &&
|
||||||
|
isOutOfViewport(dropdownSection, originalPosition)
|
||||||
|
) {
|
||||||
|
dropdownSection.style.top = originalPosition.top + "px";
|
||||||
|
dropdownSection.style.left = originalPosition.left + "px";
|
||||||
|
lastKnownPosition = originalPosition;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the element is out of the viewport
|
||||||
|
const isOutOfViewport = (element, position) => {
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
return (
|
||||||
|
position.left < 0 ||
|
||||||
|
position.right > viewportWidth ||
|
||||||
|
position.top < 0 ||
|
||||||
|
position.bottom > viewportHeight
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for viewport size changes (e.g., window resize) to reposition the dropdown
|
||||||
|
const handleResize = () => {
|
||||||
|
if (isOpen.value) {
|
||||||
|
positionDropdown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for scrolling to reposition the dropdown
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (isOpen.value) {
|
||||||
|
positionDropdown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Add a listener for window resize to reposition the dropdown
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
window.addEventListener("scroll", handleScroll);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// Remove the window resize listener when the component is unmounted
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="dropdown" ref="dropdownRef" v-click-away="closeMenu">
|
||||||
|
<button
|
||||||
|
@click="toggle"
|
||||||
|
class="button"
|
||||||
|
:class="{
|
||||||
|
'button-sm': size === 'sm',
|
||||||
|
'button-md': size === 'md',
|
||||||
|
'button-lg': size === 'lg',
|
||||||
|
|
||||||
|
// Filled Button
|
||||||
|
'button-primary': variant === 'primary',
|
||||||
|
'button-secondary': variant === 'secondary',
|
||||||
|
'button-info': variant === 'info',
|
||||||
|
'button-success': variant === 'success',
|
||||||
|
'button-warning': variant === 'warning',
|
||||||
|
'button-danger': variant === 'danger',
|
||||||
|
|
||||||
|
// Outline Button
|
||||||
|
'outline-primary': variant === 'primary-outline',
|
||||||
|
'outline-secondary': variant === 'secondary-outline',
|
||||||
|
'outline-info': variant === 'info-outline',
|
||||||
|
'outline-success': variant === 'success-outline',
|
||||||
|
'outline-warning': variant === 'warning-outline',
|
||||||
|
'outline-danger': variant === 'danger-outline',
|
||||||
|
|
||||||
|
//Text Button
|
||||||
|
'texts-primary': variant === 'primary-text',
|
||||||
|
'texts-secondary': variant === 'secondary-text',
|
||||||
|
'texts-info': variant === 'info-text',
|
||||||
|
'texts-success': variant === 'success-text',
|
||||||
|
'texts-warning': variant === 'warning-text',
|
||||||
|
'texts-danger': variant === 'danger-text',
|
||||||
|
}"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<slot v-if="$slots.title" name="title"></slot>
|
||||||
|
<span v-else>{{ props.title }}</span>
|
||||||
|
<Icon
|
||||||
|
v-if="position === 'bottom'"
|
||||||
|
name="ic:outline-keyboard-arrow-down"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
v-else-if="position === 'top'"
|
||||||
|
name="ic:outline-keyboard-arrow-up"
|
||||||
|
/>
|
||||||
|
<Icon v-else-if="position === 'left'" name="ic:outline-chevron-left" />
|
||||||
|
<Icon v-else-if="position === 'right'" name="ic:outline-chevron-right" />
|
||||||
|
</button>
|
||||||
|
<section
|
||||||
|
class="dropdown-section"
|
||||||
|
:class="{
|
||||||
|
'list-bottom-sm': position == 'bottom' && size == 'sm',
|
||||||
|
'list-bottom-md': position == 'bottom' && size == 'md',
|
||||||
|
'list-bottom-lg': position == 'bottom' && size == 'lg',
|
||||||
|
'list-top-sm': position == 'top' && size == 'sm',
|
||||||
|
'list-top-md': position == 'top' && size == 'md',
|
||||||
|
'list-top-lg': position == 'top' && size == 'lg',
|
||||||
|
'list-left': position == 'left',
|
||||||
|
'list-right': position == 'right',
|
||||||
|
'list-align-right':
|
||||||
|
(position == 'bottom' || position == 'top') && textAlign == 'right',
|
||||||
|
}"
|
||||||
|
:style="`min-width: ${itemSize}`"
|
||||||
|
v-show="isOpen"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
21
components/RsDropdownItem.vue
Normal file
21
components/RsDropdownItem.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup>
|
||||||
|
const emits = defineEmits(["click"]);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
divider: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const clickEvent = () => {
|
||||||
|
emits("click");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<hr v-if="divider" />
|
||||||
|
<div @click="clickEvent" class="dropdown-item">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
45
components/RsFieldset.vue
Normal file
45
components/RsFieldset.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup>
|
||||||
|
// const props = defineProps({
|
||||||
|
// context: Object,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const label = props.context.label || "";
|
||||||
|
// const border = props.context.border || true;
|
||||||
|
// const borderColour = props.context.borderColour || "gray";
|
||||||
|
// const borderWidth = props.context.borderWidth || "1px";
|
||||||
|
// const borderRadius = props.context.borderRadius || "0.375rem";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: String,
|
||||||
|
border: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
borderColour: {
|
||||||
|
type: String,
|
||||||
|
default: "rgb(226 232 240)",
|
||||||
|
},
|
||||||
|
borderWidth: {
|
||||||
|
type: String,
|
||||||
|
default: "1px",
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
type: String,
|
||||||
|
default: "0.375rem",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<fieldset
|
||||||
|
class="p-3"
|
||||||
|
:style="{
|
||||||
|
border: border ? '1px solid ' + borderColour : 'none',
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
borderWidth: borderWidth,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<legend class="font-bold text-sm">{{ label }}</legend>
|
||||||
|
<slot></slot>
|
||||||
|
</fieldset>
|
||||||
|
</template>
|
||||||
149
components/RsModal.vue
Normal file
149
components/RsModal.vue
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<script setup>
|
||||||
|
const emits = defineEmits(["update:modelValue"]);
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: "md",
|
||||||
|
},
|
||||||
|
dialogClass: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: String,
|
||||||
|
default: "top",
|
||||||
|
},
|
||||||
|
hideOverlay: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
okOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
okTitle: {
|
||||||
|
type: String,
|
||||||
|
default: "OK",
|
||||||
|
},
|
||||||
|
cancelOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
cancelTitle: {
|
||||||
|
type: String,
|
||||||
|
default: "Cancel",
|
||||||
|
},
|
||||||
|
okCallback: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
cancelCallback: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
hideFooter: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
overlayClose: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: "70vh",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
emits("update:modelValue", false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateCancelCallback = () => {
|
||||||
|
if (props.cancelCallback == "() => {}") closeModal();
|
||||||
|
else props.cancelCallback();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
if (val) document.body.style.overflow = "hidden";
|
||||||
|
else document.body.style.overflow = "auto";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<transition-group name="fade">
|
||||||
|
<div
|
||||||
|
v-if="modelValue"
|
||||||
|
@click.self="overlayClose ? closeModal() : ''"
|
||||||
|
class="modal"
|
||||||
|
style="z-index: 1000"
|
||||||
|
:class="{
|
||||||
|
'modal-top': position == 'top',
|
||||||
|
'modal-center': position == 'center',
|
||||||
|
'modal-end': position == 'bottom',
|
||||||
|
'modal-hide-overlay': hideOverlay,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-show="modelValue"
|
||||||
|
class="modal-dialog"
|
||||||
|
:class="dialogClass"
|
||||||
|
:style="{
|
||||||
|
width: size == 'sm' ? '300px' : size == 'md' ? '500px' : '800px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 v-if="!$slots.header">
|
||||||
|
{{ title }}
|
||||||
|
</h4>
|
||||||
|
<slot name="header"></slot>
|
||||||
|
<Icon
|
||||||
|
@click="closeModal"
|
||||||
|
class="hover:text-gray-800 cursor-pointer"
|
||||||
|
name="ic:round-close"
|
||||||
|
></Icon>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<NuxtScrollbar
|
||||||
|
:style="{
|
||||||
|
'max-height': height,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot name="body"></slot>
|
||||||
|
<slot v-if="!$slots.body"></slot>
|
||||||
|
</NuxtScrollbar>
|
||||||
|
</div>
|
||||||
|
<div v-if="!hideFooter" class="modal-footer">
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
<rs-button
|
||||||
|
v-if="!$slots.footer && !okOnly"
|
||||||
|
@click="validateCancelCallback"
|
||||||
|
variant="primary-text"
|
||||||
|
>
|
||||||
|
{{ cancelTitle }}</rs-button
|
||||||
|
>
|
||||||
|
<rs-button
|
||||||
|
v-if="!$slots.footer && !cancelOnly"
|
||||||
|
@click="okCallback"
|
||||||
|
>{{ okTitle }}</rs-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
63
components/RsProgressBar.vue
Normal file
63
components/RsProgressBar.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: Number,
|
||||||
|
default: 100,
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: "primary",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: "md",
|
||||||
|
},
|
||||||
|
showValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="progress-wrapper">
|
||||||
|
<div class="progress-label">{{ label }}</div>
|
||||||
|
<div
|
||||||
|
class="progress"
|
||||||
|
:class="{
|
||||||
|
'progress-sm': size === 'sm',
|
||||||
|
'progress-md': size === 'md',
|
||||||
|
'progress-lg': size === 'lg',
|
||||||
|
'progress-primary': variant == 'primary',
|
||||||
|
'progress-secondary': variant == 'secondary',
|
||||||
|
'progress-success': variant == 'info',
|
||||||
|
'progress-info': variant == 'success',
|
||||||
|
'progress-warning': variant == 'warning',
|
||||||
|
'progress-danger': variant == 'danger',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="progress-bar"
|
||||||
|
:class="{
|
||||||
|
primary: variant == 'primary',
|
||||||
|
secondary: variant == 'secondary',
|
||||||
|
info: variant == 'info',
|
||||||
|
success: variant == 'success',
|
||||||
|
warning: variant == 'warning',
|
||||||
|
danger: variant == 'danger',
|
||||||
|
}"
|
||||||
|
:style="{ width: (value / max) * 100 + '%' }"
|
||||||
|
>
|
||||||
|
<span class="text-xs" v-if="showValue">{{ value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
230
components/RsTab.vue
Normal file
230
components/RsTab.vue
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: "primary",
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: "default",
|
||||||
|
},
|
||||||
|
vertical: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
justify: {
|
||||||
|
type: String,
|
||||||
|
default: "left",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Slots
|
||||||
|
const slots = useSlots();
|
||||||
|
|
||||||
|
const tabs = ref(slots.default()?.map((tab) => tab.props).filter(props => props && props.title) || []);
|
||||||
|
const selectedTitle = ref(tabs.value.length > 0 ? tabs.value[0]["title"] : "");
|
||||||
|
|
||||||
|
tabs.value.forEach((tab) => {
|
||||||
|
if (typeof tab.active !== "undefined" && tab.active) {
|
||||||
|
selectedTitle.value = tab.title;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
provide("selectedTitle", selectedTitle);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<client-only>
|
||||||
|
<div
|
||||||
|
class="tab"
|
||||||
|
:class="{
|
||||||
|
vertical: vertical,
|
||||||
|
'tab-card': type === 'card' && !vertical,
|
||||||
|
'card-vertical': type === 'card' && vertical,
|
||||||
|
'card-primary': type === 'card' && variant === 'primary',
|
||||||
|
'card-secondary': type === 'card' && variant === 'secondary',
|
||||||
|
'card-success': type === 'card' && variant === 'success',
|
||||||
|
'card-danger': type === 'card' && variant === 'danger',
|
||||||
|
'card-warning': type === 'card' && variant === 'warning',
|
||||||
|
'card-info': type === 'card' && variant === 'info',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
class="tab-nav"
|
||||||
|
:class="{
|
||||||
|
'tab-nav-card': type === 'card' && !vertical,
|
||||||
|
'tab-nav-card card-vertical': type === 'card' && vertical,
|
||||||
|
vertical: vertical,
|
||||||
|
'vertical-fill': vertical && fill,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="tab-item"
|
||||||
|
:class="{
|
||||||
|
fill: fill,
|
||||||
|
border: type === 'border',
|
||||||
|
'border-horizontal': type === 'border' && !vertical,
|
||||||
|
'border-horizontal-active':
|
||||||
|
selectedTitle === val.title && type === 'border' && !vertical,
|
||||||
|
'border-vertical': type === 'border' && vertical,
|
||||||
|
'border-vertical-active':
|
||||||
|
selectedTitle === val.title && type === 'border' && vertical,
|
||||||
|
|
||||||
|
// Variant Color for Border Type
|
||||||
|
'border-hover-primary': type === 'border' && variant == 'primary',
|
||||||
|
'border-hover-secondary':
|
||||||
|
type === 'border' && variant == 'secondary',
|
||||||
|
'border-hover-info': type === 'border' && variant == 'info',
|
||||||
|
'border-hover-success': type === 'border' && variant == 'success',
|
||||||
|
'border-hover-warning': type === 'border' && variant == 'warning',
|
||||||
|
'border-hover-danger': type === 'border' && variant == 'danger',
|
||||||
|
|
||||||
|
// Variant Color for Border Type Active
|
||||||
|
'border-active-primary':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'border' &&
|
||||||
|
variant == 'primary',
|
||||||
|
'border-active-secondary':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'border' &&
|
||||||
|
variant == 'secondary',
|
||||||
|
'border-active-info':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'border' &&
|
||||||
|
variant == 'info',
|
||||||
|
'border-active-success':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'border' &&
|
||||||
|
variant == 'success',
|
||||||
|
'border-active-warning':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'border' &&
|
||||||
|
variant == 'warning',
|
||||||
|
'border-active-danger':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'border' &&
|
||||||
|
variant == 'danger',
|
||||||
|
}"
|
||||||
|
role="presentation"
|
||||||
|
v-for="(val, index) in tabs"
|
||||||
|
:key="index"
|
||||||
|
@click="selectedTitle = val.title"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="tab-item-link"
|
||||||
|
:class="{
|
||||||
|
default: type === 'default' && !vertical,
|
||||||
|
'default-vertical': type === 'default' && vertical,
|
||||||
|
'default-active':
|
||||||
|
selectedTitle === val.title && type === 'default' && !vertical,
|
||||||
|
'default-vertical-active':
|
||||||
|
selectedTitle === val.title && type === 'default' && vertical,
|
||||||
|
|
||||||
|
// Variant hover for default type
|
||||||
|
'default-hover-primary':
|
||||||
|
type === 'default' && variant == 'primary',
|
||||||
|
'default-hover-secondary':
|
||||||
|
type === 'default' && variant == 'secondary',
|
||||||
|
'default-hover-info': type === 'default' && variant == 'info',
|
||||||
|
'default-hover-success':
|
||||||
|
type === 'default' && variant == 'success',
|
||||||
|
'default-hover-warning':
|
||||||
|
type === 'default' && variant == 'warning',
|
||||||
|
'default-hover-danger': type === 'default' && variant == 'danger',
|
||||||
|
|
||||||
|
// Variant Color for default type Active
|
||||||
|
'default-primary':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'default' &&
|
||||||
|
variant == 'primary',
|
||||||
|
'default-secondary':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'default' &&
|
||||||
|
variant == 'secondary',
|
||||||
|
'default-info':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'default' &&
|
||||||
|
variant == 'info',
|
||||||
|
'default-success':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'default' &&
|
||||||
|
variant == 'success',
|
||||||
|
'default-warning':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'default' &&
|
||||||
|
variant == 'warning',
|
||||||
|
'default-danger':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'default' &&
|
||||||
|
variant == 'danger',
|
||||||
|
|
||||||
|
'link-card': type === 'card' && !vertical,
|
||||||
|
'link-card-vertical': type === 'card' && vertical,
|
||||||
|
|
||||||
|
// Variant Color for card type
|
||||||
|
'link-card-primary': type === 'card' && variant == 'primary',
|
||||||
|
'link-card-secondary': type === 'card' && variant == 'secondary',
|
||||||
|
'link-card-info': type === 'card' && variant == 'info',
|
||||||
|
'link-card-success': type === 'card' && variant == 'success',
|
||||||
|
'link-card-warning': type === 'card' && variant == 'warning',
|
||||||
|
'link-card-danger': type === 'card' && variant == 'danger',
|
||||||
|
|
||||||
|
// Variant Color for card type Active
|
||||||
|
'link-card-primary-active':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'card' &&
|
||||||
|
variant == 'primary',
|
||||||
|
'link-card-secondary-active':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'card' &&
|
||||||
|
variant == 'secondary',
|
||||||
|
'link-card-info-active':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'card' &&
|
||||||
|
variant == 'info',
|
||||||
|
'link-card-success-active':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'card' &&
|
||||||
|
variant == 'success',
|
||||||
|
'link-card-warning-active':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'card' &&
|
||||||
|
variant == 'warning',
|
||||||
|
'link-card-danger-active':
|
||||||
|
selectedTitle === val.title &&
|
||||||
|
type === 'card' &&
|
||||||
|
variant == 'danger',
|
||||||
|
|
||||||
|
'link-justify-left': justify == 'left',
|
||||||
|
'link-justify-center': justify == 'center',
|
||||||
|
'link-justify-right': justify == 'right',
|
||||||
|
}"
|
||||||
|
>{{ val.title }}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div
|
||||||
|
class="tab-content"
|
||||||
|
:class="{
|
||||||
|
'content-vertical': vertical && !fill,
|
||||||
|
'content-vertical-fill': vertical && fill,
|
||||||
|
'content-border': type === 'border' && !vertical,
|
||||||
|
'content-border-vertical': type === 'border' && vertical,
|
||||||
|
'content-border-primary': type === 'border' && variant === 'primary',
|
||||||
|
'content-border-secondary':
|
||||||
|
type === 'border' && variant === 'secondary',
|
||||||
|
'content-border-info': type === 'border' && variant === 'info',
|
||||||
|
'content-border-success': type === 'border' && variant === 'success',
|
||||||
|
'content-border-warning': type === 'border' && variant === 'warning',
|
||||||
|
'content-border-danger': type === 'border' && variant === 'danger',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</client-only>
|
||||||
|
</template>
|
||||||
20
components/RsTabItem.vue
Normal file
20
components/RsTabItem.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedTitle = inject("selectedTitle");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="tab-pane" v-if="selectedTitle === title">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
808
components/RsTable.vue
Normal file
808
components/RsTable.vue
Normal file
@@ -0,0 +1,808 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useLayoutStore } from "~/stores/layout";
|
||||||
|
import { useWindowSize } from "vue-window-size";
|
||||||
|
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
const mobileWidth = layoutStore.mobileWidth;
|
||||||
|
|
||||||
|
const { width } = useWindowSize();
|
||||||
|
const windowWidth = ref(width);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
field: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
basic: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
advanced: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
variant: "default",
|
||||||
|
striped: false,
|
||||||
|
bordered: false,
|
||||||
|
borderless: false,
|
||||||
|
hover: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
optionsAdvanced: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
responsive: false,
|
||||||
|
outsideBorder: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
pageSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
sort: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
column: "",
|
||||||
|
direction: "asc",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default varaiable
|
||||||
|
const columnTitle = ref([]);
|
||||||
|
const dataTable = ref([]);
|
||||||
|
const dataTitle = ref([]);
|
||||||
|
const dataLength = ref(0);
|
||||||
|
|
||||||
|
// Advanced Option Variable
|
||||||
|
const currentSort = ref(0);
|
||||||
|
const currentSortDir = ref("asc");
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const pageSize = ref(props.pageSize);
|
||||||
|
const maxPageShown = ref(3);
|
||||||
|
|
||||||
|
// Searching Variable
|
||||||
|
const keyword = ref("");
|
||||||
|
|
||||||
|
// Filtering Variable
|
||||||
|
const filter = ref([]);
|
||||||
|
const openFilter = ref(false);
|
||||||
|
|
||||||
|
const hideTable = ref(false);
|
||||||
|
|
||||||
|
// Other Variable
|
||||||
|
const sortColumnFirstTime = ref(false);
|
||||||
|
|
||||||
|
const isDesktop = computed(() => {
|
||||||
|
return windowWidth.value >= mobileWidth ? true : false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (props.optionsAdvanced.responsive) {
|
||||||
|
if (isDesktop.value) {
|
||||||
|
hideTable.value = false;
|
||||||
|
} else {
|
||||||
|
hideTable.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const camelCasetoTitle = (str, exclusions = []) => {
|
||||||
|
if (exclusions.includes(str)) {
|
||||||
|
return str.replace(/([A-Z])/g, " $1").trim();
|
||||||
|
} else if (/\(.*\)/.test(str)) {
|
||||||
|
return str; // if the string contains parentheses, return the original string
|
||||||
|
} else {
|
||||||
|
return str.replace(/([A-Z])/g, " $1").replace(/^./, (str) => {
|
||||||
|
return str.toUpperCase();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const spacingCharactertoCamelCase = (array) => {
|
||||||
|
// Loop array string and convert to camel case
|
||||||
|
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
array.forEach((element) => {
|
||||||
|
// Handle both string elements and object elements with key property
|
||||||
|
const stringElement = typeof element === 'string' ? element : element.key;
|
||||||
|
|
||||||
|
if (stringElement && stringElement.charAt(0) == stringElement.charAt(0).toUpperCase()) {
|
||||||
|
// Camelcase the string and remove spacing
|
||||||
|
// and if there is () in the string, do Uppercase inside the () and dont spacing it
|
||||||
|
|
||||||
|
let camelCase = stringElement
|
||||||
|
.replace(/([A-Z])/g, " $1")
|
||||||
|
.replace(/^./, (str) => {
|
||||||
|
return str.toUpperCase();
|
||||||
|
})
|
||||||
|
.replace(/\s/g, "");
|
||||||
|
|
||||||
|
let resultCamelCase = camelCase.replace(/\(([^)]+)\)/, (str) => {
|
||||||
|
return str.toUpperCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
result.push(resultCamelCase);
|
||||||
|
} else {
|
||||||
|
result.push(stringElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log(result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// watch props.data change and redo all the data
|
||||||
|
watch(
|
||||||
|
() => [props.data, props.field],
|
||||||
|
() => {
|
||||||
|
if (props.data && props.data.length > 0) {
|
||||||
|
dataTable.value = props.data;
|
||||||
|
dataLength.value = props.data.length;
|
||||||
|
if (props.field && props.field.length > 0) {
|
||||||
|
columnTitle.value = spacingCharactertoCamelCase(props.field);
|
||||||
|
dataTitle.value = spacingCharactertoCamelCase(props.field);
|
||||||
|
} else {
|
||||||
|
columnTitle.value = Object.keys(dataTable.value[0]);
|
||||||
|
dataTitle.value = Object.keys(dataTable.value[0]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dataTable.value = [];
|
||||||
|
dataLength.value = 0;
|
||||||
|
columnTitle.value = [];
|
||||||
|
dataTitle.value = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const setColumnTitle = (data) => {
|
||||||
|
try {
|
||||||
|
if (props.field && props.field.length == 0) {
|
||||||
|
columnTitle.value = Object.keys(data);
|
||||||
|
} else {
|
||||||
|
columnTitle.value = spacingCharactertoCamelCase(props.field);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredDatabyTitle = (data, title) => {
|
||||||
|
let result = "";
|
||||||
|
try {
|
||||||
|
if (props.field && props.field.length == 0) {
|
||||||
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
|
if (key === title) {
|
||||||
|
result = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Get index title from columnTitle
|
||||||
|
let index = columnTitle.value.indexOf(title);
|
||||||
|
|
||||||
|
// Convert data json to array
|
||||||
|
let arr = Object.values(data);
|
||||||
|
|
||||||
|
result = arr[index];
|
||||||
|
}
|
||||||
|
if (result === "" || result === null) result = "-";
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (dataTable.value.length > 0) {
|
||||||
|
setColumnTitle(dataTable.value[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Computed data
|
||||||
|
const computedData = computed(() => {
|
||||||
|
let result = [];
|
||||||
|
let totalData = 0;
|
||||||
|
result = dataTable.value
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => {
|
||||||
|
let modifier = 1;
|
||||||
|
|
||||||
|
columnTitle.value.forEach((title, index) => {
|
||||||
|
// console.log(title, props.sort.column);
|
||||||
|
// First sort by column title
|
||||||
|
if (title === props.sort.column && !sortColumnFirstTime.value) {
|
||||||
|
currentSort.value = index;
|
||||||
|
currentSortDir.value = props.sort.direction;
|
||||||
|
sortColumnFirstTime.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if column title is number or string and convert spacing to camelcase
|
||||||
|
let a1 = filteredDatabyTitle(a, columnTitle.value[currentSort.value]);
|
||||||
|
let b1 = filteredDatabyTitle(b, columnTitle.value[currentSort.value]);
|
||||||
|
|
||||||
|
if (typeof a1 === "string") a1 = a1.toLowerCase();
|
||||||
|
if (typeof b1 === "string") b1 = b1.toLowerCase();
|
||||||
|
|
||||||
|
// Convert string to number if possible
|
||||||
|
if (isNumeric(a1)) a1 = parseFloat(a1);
|
||||||
|
if (isNumeric(b1)) b1 = parseFloat(b1);
|
||||||
|
|
||||||
|
if (currentSortDir.value === "desc") modifier = -1;
|
||||||
|
if (a1 < b1) return -1 * modifier;
|
||||||
|
if (a1 > b1) return 1 * modifier;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
.filter((row) => {
|
||||||
|
// Search all json object if keyword not equal null
|
||||||
|
if (keyword.value === "") return true;
|
||||||
|
let result = false;
|
||||||
|
Object.entries(row).forEach(([key, value]) => {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
value.toString().toLowerCase().includes(keyword.value.toLowerCase())
|
||||||
|
) {
|
||||||
|
result = true;
|
||||||
|
currentPage.value = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.filter((_, index) => {
|
||||||
|
let start = (currentPage.value - 1) * pageSize.value;
|
||||||
|
let end = currentPage.value * pageSize.value;
|
||||||
|
totalData++;
|
||||||
|
if (index >= start && index < end) return true;
|
||||||
|
});
|
||||||
|
dataLength.value = totalData;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isNumeric = (n) => {
|
||||||
|
return !isNaN(parseFloat(n)) && isFinite(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalEntries = computed(() => {
|
||||||
|
return dataLength.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sort = (index) => {
|
||||||
|
if (index === currentSort.value) {
|
||||||
|
currentSortDir.value = currentSortDir.value === "asc" ? "desc" : "asc";
|
||||||
|
} else if (index !== currentSort.value && currentSortDir.value == "desc") {
|
||||||
|
currentSortDir.value = "asc";
|
||||||
|
}
|
||||||
|
currentSort.value = index;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pages = computed(() => {
|
||||||
|
let totalPG = Math.ceil(dataLength.value / pageSize.value);
|
||||||
|
const numShown = Math.min(maxPageShown.value, totalPG);
|
||||||
|
let first = currentPage.value - Math.floor(numShown / 2);
|
||||||
|
first = Math.max(first, 1);
|
||||||
|
first = Math.min(first, totalPG - numShown + 1);
|
||||||
|
return [...Array(numShown)].map((k, i) => i + first);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPage = computed(() => {
|
||||||
|
return Math.ceil(dataLength.value / pageSize.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageChange = (page) => {
|
||||||
|
currentPage.value = page;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextPage = () => {
|
||||||
|
if (currentPage.value * pageSize.value < dataLength.value)
|
||||||
|
currentPage.value++;
|
||||||
|
};
|
||||||
|
const prevPage = () => {
|
||||||
|
if (currentPage.value > 1) currentPage.value--;
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstPage = () => {
|
||||||
|
currentPage.value = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const lastPage = () => {
|
||||||
|
currentPage.value = totalPage.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideColumn = (key) => {
|
||||||
|
if (!getFilter(key)) {
|
||||||
|
// insert into filter variable to tell there is a change in filter
|
||||||
|
setFilter(key, "hide", true);
|
||||||
|
} else {
|
||||||
|
// update filter variable to tell there is a change in filter
|
||||||
|
setFilter(key, "hide", false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setFilter = (key, action, condition) => {
|
||||||
|
// Check if key exist inside filter
|
||||||
|
let index = filter.value.findIndex((item) => item.key === key);
|
||||||
|
|
||||||
|
if (index == -1) {
|
||||||
|
// If key not exist, insert new filter
|
||||||
|
filter.value.push({
|
||||||
|
key: key,
|
||||||
|
action: {
|
||||||
|
[action]: condition,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If key exist, update filter
|
||||||
|
filter.value[index].action[action] = condition;
|
||||||
|
// console.log(filter.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilter = (key) => {
|
||||||
|
let result = false;
|
||||||
|
filter.value.forEach((item) => {
|
||||||
|
if (item.key === key) {
|
||||||
|
result = item.action.hide;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch filter.value
|
||||||
|
watch(
|
||||||
|
() => filter.value,
|
||||||
|
() => {
|
||||||
|
// console.log(filter.value);
|
||||||
|
// Loop json object filter.value
|
||||||
|
filter.value.forEach((item) => {
|
||||||
|
// Hide Column
|
||||||
|
if (item.action.hide) {
|
||||||
|
// Get index title from columnTitle
|
||||||
|
let index = columnTitle.value.indexOf(item.key);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
// Remove column from columnTitle
|
||||||
|
columnTitle.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
} else if (!item.action.hide) {
|
||||||
|
// Get index title from dataTitle
|
||||||
|
let indexData = dataTitle.value.indexOf(item.key);
|
||||||
|
|
||||||
|
if (!columnTitle.value.includes(item.key)) {
|
||||||
|
// Add Column back to its original position
|
||||||
|
columnTitle.value.splice(indexData, 0, item.key);
|
||||||
|
|
||||||
|
// Sort the columnTitle like dataTitle
|
||||||
|
columnTitle.value.sort((a, b) => {
|
||||||
|
let indexA = dataTitle.value.indexOf(a);
|
||||||
|
let indexB = dataTitle.value.indexOf(b);
|
||||||
|
return indexA - indexB;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterComputed = computed(() => {
|
||||||
|
let result = [];
|
||||||
|
let i = 0;
|
||||||
|
filter.value.forEach((item) => {
|
||||||
|
if (item.action.hide) {
|
||||||
|
result.push({
|
||||||
|
title: item.key,
|
||||||
|
hide: item.action.hide,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
});
|
||||||
|
// console.log(result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
// watch pinia getter windowWidth
|
||||||
|
watch(
|
||||||
|
() => windowWidth.value,
|
||||||
|
() => {
|
||||||
|
if (props.optionsAdvanced.responsive) {
|
||||||
|
if (windowWidth.value <= mobileWidth) {
|
||||||
|
hideTable.value = true;
|
||||||
|
} else {
|
||||||
|
hideTable.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="dataTable && dataTable.length > 0"
|
||||||
|
class="table-wrapper"
|
||||||
|
:class="{
|
||||||
|
'!border': advanced && !hideTable && optionsAdvanced.outsideBorder,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="table-header"
|
||||||
|
:class="{
|
||||||
|
open: openFilter,
|
||||||
|
'!max-h-full': !optionsAdvanced.filterable,
|
||||||
|
}"
|
||||||
|
v-if="advanced"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="table-header-filter"
|
||||||
|
:class="{
|
||||||
|
'!items-center !gap-3': !optionsAdvanced.filterable,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="flex gap-x-2">
|
||||||
|
<FormKit
|
||||||
|
v-model="keyword"
|
||||||
|
type="search"
|
||||||
|
placeholder="Search..."
|
||||||
|
outer-class="mb-0"
|
||||||
|
/>
|
||||||
|
<rs-button
|
||||||
|
v-if="optionsAdvanced.filterable"
|
||||||
|
class="!px-3 sm:!px-6"
|
||||||
|
@click="openFilter ? (openFilter = false) : (openFilter = true)"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="ic:outline-filter-alt"
|
||||||
|
class="mr-0 md:mr-1"
|
||||||
|
size="1rem"
|
||||||
|
/>
|
||||||
|
<span class="hidden sm:block">Filter</span>
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center gap-x-2">
|
||||||
|
<span class="text-[rgb(var(--text-color))]">Result per page:</span>
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
v-model="pageSize"
|
||||||
|
:options="[5, 10, 25, 100]"
|
||||||
|
outer-class="mb-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap items-center justify-start gap-x-3"
|
||||||
|
v-if="optionsAdvanced.filterable"
|
||||||
|
>
|
||||||
|
<rs-dropdown
|
||||||
|
:title="camelCasetoTitle(val)"
|
||||||
|
size="sm"
|
||||||
|
class="mt-3"
|
||||||
|
v-for="(val, index) in dataTitle"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<rs-dropdown-item @click="hideColumn(val)">
|
||||||
|
{{ getFilter(val) ? "Show Column" : "Hide Column" }}
|
||||||
|
<Icon
|
||||||
|
:name="getFilter(val) ? 'mdi:eye-outline' : 'mdi:eye-off-outline'"
|
||||||
|
size="1rem"
|
||||||
|
class="ml-auto"
|
||||||
|
></Icon>
|
||||||
|
</rs-dropdown-item>
|
||||||
|
</rs-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="filterComputed.length > 0"
|
||||||
|
class="table-header-filter-list w-full m-4"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center justify-start gap-x-2">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center gap-x-2 border border-primary text-primary rounded-lg py-1 px-2"
|
||||||
|
v-for="(val, index) in filterComputed"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
{{ val ? camelCasetoTitle(val.title) : "" }}
|
||||||
|
<Icon
|
||||||
|
name="ic:round-close"
|
||||||
|
class="mr-0 md:mr-1 hover:text-red-500 cursor-pointer"
|
||||||
|
size="1rem"
|
||||||
|
@click="hideColumn(val.title)"
|
||||||
|
></Icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full overflow-x-auto">
|
||||||
|
<client-only>
|
||||||
|
<table
|
||||||
|
v-if="!hideTable"
|
||||||
|
class="table-content"
|
||||||
|
:class="{
|
||||||
|
'!border-y !border-0 border-[rgb(var(--bg-1))]': advanced,
|
||||||
|
'table-fixed': options.fixed,
|
||||||
|
'table-auto': !options.fixed,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<thead
|
||||||
|
class="text-left border-[rgb(var(--border-color))]"
|
||||||
|
:class="{
|
||||||
|
'border-y': !options.borderless,
|
||||||
|
'border-[rgb(var(--border-color))] bg-[rgb(var(--bg-2))]':
|
||||||
|
options.variant === 'default',
|
||||||
|
'border-primary/50 bg-primary text-white':
|
||||||
|
options.variant === 'primary',
|
||||||
|
'border-secondary/50 bg-secondary text-white':
|
||||||
|
options.variant === 'secondary',
|
||||||
|
'border-info/50 bg-info text-white ': options.variant === 'info',
|
||||||
|
'border-success/50 bg-success text-white':
|
||||||
|
options.variant === 'success',
|
||||||
|
'border-warning/50 bg-warning text-white':
|
||||||
|
options.variant === 'warning',
|
||||||
|
'border-danger/50 bg-danger text-white':
|
||||||
|
options.variant === 'danger',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
class="relative py-3 pl-5 pr-8 whitespace-nowrap"
|
||||||
|
:class="{
|
||||||
|
'border-r last:border-l last:border-r-0':
|
||||||
|
options.bordered && !options.borderless,
|
||||||
|
'border-[rgb(var(--border-color))]':
|
||||||
|
options.variant === 'default',
|
||||||
|
'border-primary/80': options.variant === 'primary',
|
||||||
|
'border-secondary/80': options.variant === 'secondary',
|
||||||
|
'border-info/80': options.variant === 'info',
|
||||||
|
'border-success/80': options.variant === 'success',
|
||||||
|
'border-warning/80': options.variant === 'warning',
|
||||||
|
'border-danger/80': options.variant === 'danger',
|
||||||
|
'w-36': options.fixed,
|
||||||
|
'cursor-pointer': optionsAdvanced.sortable && advanced,
|
||||||
|
}"
|
||||||
|
style="min-width: 100px"
|
||||||
|
@click="
|
||||||
|
optionsAdvanced.sortable && advanced ? sort(index) : null
|
||||||
|
"
|
||||||
|
v-for="(val, index) in columnTitle"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
{{ camelCasetoTitle(val) }}
|
||||||
|
<div
|
||||||
|
v-if="optionsAdvanced.sortable && advanced"
|
||||||
|
class="sortable"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
class="absolute top-3 right-2 opacity-20"
|
||||||
|
size="1.25rem"
|
||||||
|
name="carbon:chevron-sort"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
v-if="currentSort == index && currentSortDir == 'asc'"
|
||||||
|
class="absolute top-3 right-2 opacity-50"
|
||||||
|
size="1.25rem"
|
||||||
|
name="carbon:chevron-sort-up"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
v-else-if="currentSort == index && currentSortDir == 'desc'"
|
||||||
|
class="absolute top-3 right-2 opacity-50"
|
||||||
|
size="1.25rem"
|
||||||
|
name="carbon:chevron-sort-down"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
:class="{
|
||||||
|
'border-y border-[rgb(var(--border-color))]':
|
||||||
|
!options.bordered && !options.borderless,
|
||||||
|
'border-b': options.bordered && !options.borderless,
|
||||||
|
'border-b-0': options.borderless,
|
||||||
|
'border-[rgb(var(--border-color))] odd:bg-[rgb(var(--bg-1))] even:bg-[rgb(var(--bg-2))]':
|
||||||
|
options.variant === 'default' && options.striped,
|
||||||
|
'border-primary/20 odd:bg-white even:bg-primary/5':
|
||||||
|
options.variant === 'primary' && options.striped,
|
||||||
|
'border-secondary/20 odd:bg-white even:bg-secondary/5':
|
||||||
|
options.variant === 'secondary' && options.striped,
|
||||||
|
'border-info/20 odd:bg-white even:bg-info/5':
|
||||||
|
options.variant === 'info' && options.striped,
|
||||||
|
'border-success/20 odd:bg-white even:bg-success/5':
|
||||||
|
options.variant === 'success' && options.striped,
|
||||||
|
'border-warning/20 odd:bg-white even:bg-warning/5':
|
||||||
|
options.variant === 'warning' && options.striped,
|
||||||
|
'border-danger/20 odd:bg-white even:bg-danger/5':
|
||||||
|
options.variant === 'danger' && options.striped,
|
||||||
|
'cursor-pointer hover:bg-slate-300':
|
||||||
|
options.hover && options.variant === 'default',
|
||||||
|
'cursor-pointer hover:bg-primary/5':
|
||||||
|
options.hover && options.variant === 'primary',
|
||||||
|
'cursor-pointer hover:bg-secondary/5':
|
||||||
|
options.hover && options.variant === 'secondary',
|
||||||
|
'cursor-pointer hover:bg-info/5':
|
||||||
|
options.hover && options.variant === 'info',
|
||||||
|
'cursor-pointer hover:bg-success/5':
|
||||||
|
options.hover && options.variant === 'success',
|
||||||
|
'cursor-pointer hover:bg-warning/5':
|
||||||
|
options.hover && options.variant === 'warning',
|
||||||
|
'cursor-pointer hover:bg-danger/5':
|
||||||
|
options.hover && options.variant === 'danger',
|
||||||
|
}"
|
||||||
|
v-for="(val1, index1) in computedData"
|
||||||
|
:key="index1"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="p-4 pl-5 break-words"
|
||||||
|
:class="{
|
||||||
|
'border-r last:border-l last:border-r-0':
|
||||||
|
options.bordered && !options.borderless,
|
||||||
|
'border-[rgb(var(--border-color))]':
|
||||||
|
options.variant === 'default',
|
||||||
|
'border-primary/20': options.variant === 'primary',
|
||||||
|
'border-secondary/20': options.variant === 'secondary',
|
||||||
|
'border-info/20': options.variant === 'info',
|
||||||
|
'border-success/20': options.variant === 'success',
|
||||||
|
'border-warning/20': options.variant === 'warning',
|
||||||
|
'border-danger/20': options.variant === 'danger',
|
||||||
|
}"
|
||||||
|
v-for="(val2, index2) in columnTitle"
|
||||||
|
:key="index2"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
:name="val2"
|
||||||
|
:text="filteredDatabyTitle(val1, val2)"
|
||||||
|
:value="val1"
|
||||||
|
>
|
||||||
|
{{ filteredDatabyTitle(val1, val2) }}
|
||||||
|
</slot>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div v-else>
|
||||||
|
<rs-collapse v-if="computedData.length > 0" accordion>
|
||||||
|
<rs-collapse-item v-for="(val, index) in computedData" :key="index">
|
||||||
|
<template #title>
|
||||||
|
<div class="grid grid-cols-2">
|
||||||
|
<div class="flex flex-col col-span-1">
|
||||||
|
<span class="font-semibold leading-tight">
|
||||||
|
{{ Object.values(val)[0] }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm"> {{ Object.values(val)[1] }} </span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end items-center col-span-1">
|
||||||
|
<div class="mr-4">
|
||||||
|
{{ Object.values(val)[computedData.length] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center even:bg-inherit odd:bg-[rgb(var(--bg-1))] rounded-lg p-3"
|
||||||
|
v-for="(val1, index1) in Object.entries(val).slice(
|
||||||
|
2,
|
||||||
|
Object.entries(val).length
|
||||||
|
)"
|
||||||
|
:key="index1"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ camelCasetoTitle(val1[0]) }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ val1[1] }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-collapse-item>
|
||||||
|
</rs-collapse>
|
||||||
|
</div>
|
||||||
|
</client-only>
|
||||||
|
</div>
|
||||||
|
<div v-if="advanced" class="table-footer">
|
||||||
|
<div class="flex justify-center items-center gap-x-2">
|
||||||
|
<span class="text-sm text-[rgb(var(--text-color))] hidden md:block"
|
||||||
|
>Showing {{ pageSize * currentPage - pageSize + 1 }} to
|
||||||
|
{{ pageSize * currentPage }} of {{ totalEntries }} entries</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="table-footer-page">
|
||||||
|
<rs-button
|
||||||
|
:variant="`${
|
||||||
|
options.variant == 'default' ? 'primary' : options.variant
|
||||||
|
}-outline`"
|
||||||
|
class="!rounded-full !p-1 !w-8 !h-8"
|
||||||
|
@click="firstPage"
|
||||||
|
:disabled="currentPage == 1"
|
||||||
|
>
|
||||||
|
<Icon name="ic:round-keyboard-double-arrow-left" size="1rem"></Icon>
|
||||||
|
</rs-button>
|
||||||
|
<rs-button
|
||||||
|
:variant="`${
|
||||||
|
options.variant == 'default' ? 'primary' : options.variant
|
||||||
|
}-outline`"
|
||||||
|
class="!rounded-full !p-1 !w-8 !h-8"
|
||||||
|
@click="prevPage"
|
||||||
|
:disabled="currentPage == 1"
|
||||||
|
>
|
||||||
|
<Icon name="ic:round-keyboard-arrow-left" size="1rem"></Icon>
|
||||||
|
</rs-button>
|
||||||
|
<rs-button
|
||||||
|
:variant="`${
|
||||||
|
currentPage == val && options.variant != 'default'
|
||||||
|
? options.variant
|
||||||
|
: currentPage == val && options.variant == 'default'
|
||||||
|
? 'primary'
|
||||||
|
: options.variant == 'default'
|
||||||
|
? 'primary-outline'
|
||||||
|
: options.variant + '-outline'
|
||||||
|
}`"
|
||||||
|
class="!rounded-full !p-1 !w-8 !h-8"
|
||||||
|
v-for="(val, index) in pages"
|
||||||
|
:key="index"
|
||||||
|
@click="pageChange(val)"
|
||||||
|
>
|
||||||
|
{{ val }}
|
||||||
|
</rs-button>
|
||||||
|
<rs-button
|
||||||
|
:variant="`${
|
||||||
|
options.variant == 'default' ? 'primary' : options.variant
|
||||||
|
}-outline`"
|
||||||
|
class="!rounded-full !p-1 !w-8 !h-8"
|
||||||
|
@click="nextPage"
|
||||||
|
:disabled="currentPage == totalPage"
|
||||||
|
>
|
||||||
|
<Icon name="ic:round-keyboard-arrow-right" size="1rem"></Icon>
|
||||||
|
</rs-button>
|
||||||
|
<rs-button
|
||||||
|
:variant="`${
|
||||||
|
options.variant == 'default' ? 'primary' : options.variant
|
||||||
|
}-outline`"
|
||||||
|
class="!rounded-full !p-1 !w-8 !h-8"
|
||||||
|
@click="lastPage"
|
||||||
|
:disabled="currentPage == totalPage"
|
||||||
|
>
|
||||||
|
<Icon name="ic:round-keyboard-double-arrow-right" size="1rem"></Icon>
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="table-wrapper p-4">
|
||||||
|
<div
|
||||||
|
class="border border-[rgb(var(--border-color))] rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[rgb(var(--bg-2))] p-4 border-b border-[rgb(var(--border-color))]"
|
||||||
|
>
|
||||||
|
<h3 class="text-lg font-semibold text-[rgb(var(--text-color))]"></h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-8 text-center">
|
||||||
|
<Icon name="mdi:table-off" class="text-gray-300 mb-4" size="48px" />
|
||||||
|
<p class="text-[rgb(var(--text-color))] text-lg font-medium">
|
||||||
|
Tiada data
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-500 mt-2">
|
||||||
|
Tiada entri untuk dipaparkan pada masa ini.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
228
components/RsWizard.vue
Normal file
228
components/RsWizard.vue
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
<script setup>
|
||||||
|
import { getNode, createMessage } from "@formkit/core";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: "top",
|
||||||
|
},
|
||||||
|
steps: {
|
||||||
|
type: Array,
|
||||||
|
default: () => ["Default"],
|
||||||
|
},
|
||||||
|
currentStep: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
formSubmit: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
formAction: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
formStepRequired: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
formStepBack: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
formNavigate: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
formErrorCounter: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const step = reactive({});
|
||||||
|
const activeStep = ref(props.currentStep);
|
||||||
|
const stepNames = ref(props.steps);
|
||||||
|
const visitedSteps = ref([]);
|
||||||
|
|
||||||
|
const toLowerCase = (str) => str.toLowerCase().replace(/\s/g, "");
|
||||||
|
const stepIndex = (stepName) => stepNames.value.indexOf(stepName);
|
||||||
|
|
||||||
|
watch(activeStep, (newStep, oldStep) => {
|
||||||
|
if (oldStep && !visitedSteps.value.includes(oldStep)) {
|
||||||
|
visitedSteps.value.push(oldStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: trigger showing validation on fields
|
||||||
|
// within all visited steps
|
||||||
|
visitedSteps.value.forEach((step) => {
|
||||||
|
const node = getNode(step);
|
||||||
|
if (node != undefined)
|
||||||
|
node.walk((n) => {
|
||||||
|
n.store.set(
|
||||||
|
createMessage({
|
||||||
|
key: "submitted",
|
||||||
|
value: true,
|
||||||
|
visible: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextStep = (stepName) => {
|
||||||
|
const stepNames = Object.keys(step);
|
||||||
|
const currentIndex = stepNames.indexOf(activeStep.value);
|
||||||
|
const nextIndex = stepNames.indexOf(stepName);
|
||||||
|
|
||||||
|
if (props.formStepRequired) {
|
||||||
|
if (!props.formStepBack) {
|
||||||
|
if (nextIndex > currentIndex) {
|
||||||
|
if (step[activeStep.value].valid) {
|
||||||
|
activeStep.value = stepName;
|
||||||
|
} else {
|
||||||
|
const node = getNode(activeStep.value);
|
||||||
|
if (node)
|
||||||
|
node.walk((n) => {
|
||||||
|
n.store.set(
|
||||||
|
createMessage({
|
||||||
|
key: "submitted",
|
||||||
|
value: true,
|
||||||
|
visible: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (step[activeStep.value].valid || currentIndex > nextIndex) {
|
||||||
|
activeStep.value = stepName;
|
||||||
|
} else {
|
||||||
|
const node = getNode(activeStep.value);
|
||||||
|
if (node)
|
||||||
|
node.walk((n) => {
|
||||||
|
n.store.set(
|
||||||
|
createMessage({
|
||||||
|
key: "submitted",
|
||||||
|
value: true,
|
||||||
|
visible: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activeStep.value = stepName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setStep = (delta) => {
|
||||||
|
const stepNames = Object.keys(step);
|
||||||
|
const currentIndex = stepNames.indexOf(activeStep.value);
|
||||||
|
nextStep(stepNames[currentIndex + delta]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stepPlugin = (node) => {
|
||||||
|
if (node.props.type == "group") {
|
||||||
|
// builds an object of the top-level groups
|
||||||
|
step[node.name] = step[node.name] || {};
|
||||||
|
|
||||||
|
node.on("created", () => {
|
||||||
|
// use 'on created' to ensure context object is available
|
||||||
|
step[node.name].valid = toRef(node.context.state, "valid");
|
||||||
|
});
|
||||||
|
|
||||||
|
// listen for changes in error count and store it
|
||||||
|
node.on("count:errors", ({ payload: count }) => {
|
||||||
|
step[node.name].errorCount = count;
|
||||||
|
});
|
||||||
|
|
||||||
|
// listen for changes in count of blocking validations messages
|
||||||
|
node.on("count:blocking", ({ payload: count }) => {
|
||||||
|
step[node.name].blockingCount = count;
|
||||||
|
});
|
||||||
|
|
||||||
|
// set the active tab to the 1st tab
|
||||||
|
if (activeStep.value === "") {
|
||||||
|
activeStep.value = node.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop plugin inheritance to descendant nodes
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkStepValidity = (stepName) => {
|
||||||
|
if (step[stepName]) {
|
||||||
|
return (
|
||||||
|
(step[stepName].errorCount > 0 || step[stepName].blockingCount > 0) &&
|
||||||
|
visitedSteps.value.includes(stepName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormKit
|
||||||
|
type="form"
|
||||||
|
:plugins="[stepPlugin]"
|
||||||
|
:actions="formAction ? true : false"
|
||||||
|
@submit="formSubmit ? formSubmit() : ''"
|
||||||
|
:form-class="{ 'top-form': type == 'top', 'left-form': type == 'left' }"
|
||||||
|
>
|
||||||
|
<ul :class="{ 'top-steps': type == 'top', 'left-steps': type == 'left' }">
|
||||||
|
<li
|
||||||
|
v-for="(stepName, index) in stepNames"
|
||||||
|
:key="index"
|
||||||
|
:class="['step', { 'has-errors': checkStepValidity(stepName) }]"
|
||||||
|
@click="nextStep(stepName)"
|
||||||
|
:data-step-active="activeStep === stepName"
|
||||||
|
:data-step-completed="stepIndex(stepName) < stepIndex(activeStep)"
|
||||||
|
>
|
||||||
|
<div class="counter">{{ index + 1 }}.</div>
|
||||||
|
{{ stepName }}
|
||||||
|
<span
|
||||||
|
v-show="formErrorCounter"
|
||||||
|
v-if="checkStepValidity(stepName)"
|
||||||
|
class="step--errors"
|
||||||
|
v-text="step[stepName].errorCount + step[stepName].blockingCount"
|
||||||
|
/>
|
||||||
|
<div class="progress"></div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="form-wizard">
|
||||||
|
<section
|
||||||
|
v-for="(stepName, index) in stepNames"
|
||||||
|
:key="index"
|
||||||
|
v-show="activeStep === stepName"
|
||||||
|
>
|
||||||
|
<FormKit type="group" :id="stepName" :name="stepName">
|
||||||
|
<slot
|
||||||
|
:name="stepName === 'Default' ? 'default' : toLowerCase(stepName)"
|
||||||
|
>
|
||||||
|
</slot>
|
||||||
|
</FormKit>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div v-if="formNavigate" class="flex justify-between">
|
||||||
|
<FormKit
|
||||||
|
type="button"
|
||||||
|
:disabled="stepIndex(activeStep) == 0"
|
||||||
|
@click="setStep(-1)"
|
||||||
|
v-text="'Previous step'"
|
||||||
|
/>
|
||||||
|
<FormKit
|
||||||
|
type="button"
|
||||||
|
:disabled="stepIndex(activeStep) == stepNames.length - 1"
|
||||||
|
class="next"
|
||||||
|
@click="setStep(1)"
|
||||||
|
v-text="'Next step'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormKit>
|
||||||
|
</template>
|
||||||
35
components/VoiceReader.vue
Normal file
35
components/VoiceReader.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div class="voice-reader">
|
||||||
|
<rs-button
|
||||||
|
@click="toggleReading"
|
||||||
|
:variant="isReading ? 'danger' : 'primary'"
|
||||||
|
class="p-2 rounded-full"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
:name="isReading ? 'ph:ear' : 'ph:ear-slash'"
|
||||||
|
:class="['text-2xl', { 'animate-pulse text-white': isReading }]"
|
||||||
|
/>
|
||||||
|
</rs-button>
|
||||||
|
<span ref="announceElement" class="sr-only" aria-live="polite"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useVoiceReader } from "~/composables/useVoiceReader";
|
||||||
|
|
||||||
|
const { isReading, toggleReading, announceElement } = useVoiceReader();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
33
components/draggable/nested.vue
Normal file
33
components/draggable/nested.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
import DraggableNested from "~/components/draggable/nested.vue";
|
||||||
|
const props = defineProps({
|
||||||
|
tasks: {
|
||||||
|
required: true,
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<draggable
|
||||||
|
class="dragArea"
|
||||||
|
tag="ul"
|
||||||
|
:list="tasks"
|
||||||
|
:group="{ name: 'g1' }"
|
||||||
|
item-key="name"
|
||||||
|
>
|
||||||
|
<template #item="{ element }">
|
||||||
|
<li>
|
||||||
|
<p>{{ element.name }}</p>
|
||||||
|
<DraggableNested :tasks="element.tasks" />
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dragArea {
|
||||||
|
min-height: 50px;
|
||||||
|
outline: 1px dashed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
533
components/draggable/sideMenuNested.vue
Normal file
533
components/draggable/sideMenuNested.vue
Normal file
@@ -0,0 +1,533 @@
|
|||||||
|
<script setup>
|
||||||
|
import DraggableSideMenuNested from "~/components/draggable/sideMenuNested.vue";
|
||||||
|
const props = defineProps({
|
||||||
|
menus: {
|
||||||
|
required: true,
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
required: false,
|
||||||
|
default: 0,
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
parentMenu: {
|
||||||
|
required: false,
|
||||||
|
default: [],
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(["changeSideMenu"]);
|
||||||
|
|
||||||
|
const showModal = ref(false);
|
||||||
|
const type = ref(null);
|
||||||
|
const formMenu = ref({
|
||||||
|
index: null,
|
||||||
|
name: null,
|
||||||
|
title: null,
|
||||||
|
path: null,
|
||||||
|
icon: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formHeader = ref({
|
||||||
|
index: null,
|
||||||
|
header: null,
|
||||||
|
description: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const viewPermissionType = ref([
|
||||||
|
{
|
||||||
|
label: "All",
|
||||||
|
value: "all",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "User",
|
||||||
|
value: "user",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Role",
|
||||||
|
value: "role",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const viewPermissionTypeRadio = ref("");
|
||||||
|
const roleList = ref([]);
|
||||||
|
const userList = ref([]);
|
||||||
|
|
||||||
|
const selectListValue = ref([]);
|
||||||
|
|
||||||
|
const checkAll = ref(false);
|
||||||
|
|
||||||
|
// watch viewPermissionTypeRadio
|
||||||
|
watch(
|
||||||
|
viewPermissionTypeRadio,
|
||||||
|
async (val) => {
|
||||||
|
if (val == "") viewPermissionTypeRadio.value = "all";
|
||||||
|
else if (val == "user") await getUserList();
|
||||||
|
else if (val == "role") await getRoleList();
|
||||||
|
|
||||||
|
// Check if selectListValue doesnt match with user or role list then reset selectListValue
|
||||||
|
if (val == "user") {
|
||||||
|
selectListValue.value = selectListValue.value.filter((item) => {
|
||||||
|
return userList.value.some((user) => user.value == item.value);
|
||||||
|
});
|
||||||
|
} else if (val == "role") {
|
||||||
|
selectListValue.value = selectListValue.value.filter((item) => {
|
||||||
|
return roleList.value.some((role) => role.value == item.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAll.value = false;
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// watch checkAll
|
||||||
|
watch(
|
||||||
|
checkAll,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
if (viewPermissionTypeRadio.value == "user") {
|
||||||
|
selectListValue.value = userList.value.map((user) => {
|
||||||
|
return {
|
||||||
|
label: user.label,
|
||||||
|
value: user.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (viewPermissionTypeRadio.value == "role") {
|
||||||
|
selectListValue.value = roleList.value.map((role) => {
|
||||||
|
return {
|
||||||
|
label: role.label,
|
||||||
|
value: role.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectListValue.value = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const getUserList = async () => {
|
||||||
|
const { data } = await useFetch("/api/devtool/menu/user-list");
|
||||||
|
if (data.value?.statusCode === 200) {
|
||||||
|
userList.value = data.value.data.map((user) => {
|
||||||
|
return {
|
||||||
|
label: user.userUsername,
|
||||||
|
value: user.userUsername,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRoleList = async () => {
|
||||||
|
const { data } = await useFetch("/api/devtool/menu/role-list");
|
||||||
|
|
||||||
|
if (data.value?.statusCode === 200) {
|
||||||
|
roleList.value = data.value.data.map((role) => {
|
||||||
|
return {
|
||||||
|
label: role.roleName,
|
||||||
|
value: role.roleName,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clone = (obj) => {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modal functions
|
||||||
|
const openModal = () => {
|
||||||
|
showModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const assignDataMenu = (menu) => {
|
||||||
|
formMenu.value = {
|
||||||
|
index: menu.index,
|
||||||
|
name: menu.name,
|
||||||
|
title: menu.title,
|
||||||
|
path: menu.path,
|
||||||
|
icon: menu.icon,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (menu.meta?.auth?.user) {
|
||||||
|
viewPermissionTypeRadio.value = "user";
|
||||||
|
selectListValue.value = menu.meta.auth.user.map((user) => {
|
||||||
|
return {
|
||||||
|
label: user,
|
||||||
|
value: user,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (menu.meta?.auth?.role) {
|
||||||
|
viewPermissionTypeRadio.value = "role";
|
||||||
|
selectListValue.value = menu.meta.auth.role.map((role) => {
|
||||||
|
return {
|
||||||
|
label: role,
|
||||||
|
value: role,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
viewPermissionTypeRadio.value = "all";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const assignDataHeader = (header) => {
|
||||||
|
formHeader.value = {
|
||||||
|
index: props.menus.indexOf(header),
|
||||||
|
header: header.header,
|
||||||
|
description: header.description,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (header.meta?.auth?.user) {
|
||||||
|
viewPermissionTypeRadio.value = "user";
|
||||||
|
selectListValue.value = header.meta.auth.user.map((user) => {
|
||||||
|
return {
|
||||||
|
label: user,
|
||||||
|
value: user,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (header.meta?.auth?.role) {
|
||||||
|
viewPermissionTypeRadio.value = "role";
|
||||||
|
selectListValue.value = header.meta.auth.role.map((role) => {
|
||||||
|
return {
|
||||||
|
label: role,
|
||||||
|
value: role,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
viewPermissionTypeRadio.value = "all";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickOK = () => {
|
||||||
|
showModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickCancel = () => {
|
||||||
|
showModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the menus
|
||||||
|
const updateMenus = (menus) => {
|
||||||
|
emits("changeSideMenu", menus);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save the menu
|
||||||
|
const saveEditChanges = () => {
|
||||||
|
let newMenu = props.menus;
|
||||||
|
|
||||||
|
if (type.value == "menu") {
|
||||||
|
// Overwrite the props menus
|
||||||
|
props.menus.map((menu) => {
|
||||||
|
if (menu.path == formMenu.value.path) {
|
||||||
|
menu.title = formMenu.value.title;
|
||||||
|
menu.icon = formMenu.value.icon;
|
||||||
|
menu.meta = {};
|
||||||
|
|
||||||
|
// Add the meta auth based on viewPermissionTypeRadio
|
||||||
|
if (viewPermissionTypeRadio.value == "user") {
|
||||||
|
menu.meta.auth = {
|
||||||
|
user: selectListValue.value.map((user) => {
|
||||||
|
return user.value;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
} else if (viewPermissionTypeRadio.value == "role") {
|
||||||
|
menu.meta.auth = {
|
||||||
|
role: selectListValue.value.map((role) => {
|
||||||
|
return role.value;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newMenu = props.parentMenu;
|
||||||
|
} else if (type.value == "header") {
|
||||||
|
// Overwrite the props menus
|
||||||
|
newMenu = props.menus.map((header, index) => {
|
||||||
|
if (index == formHeader.value.index) {
|
||||||
|
header.header = formHeader.value.header;
|
||||||
|
header.description = formHeader.value.description;
|
||||||
|
header.meta = {};
|
||||||
|
|
||||||
|
// Add the meta auth based on viewPermissionTypeRadio
|
||||||
|
if (viewPermissionTypeRadio.value == "user") {
|
||||||
|
header.meta.auth = {
|
||||||
|
user: selectListValue.value.map((user) => {
|
||||||
|
return user.value;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
} else if (viewPermissionTypeRadio.value == "role") {
|
||||||
|
header.meta.auth = {
|
||||||
|
role: selectListValue.value.map((role) => {
|
||||||
|
return role.value;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return header;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the menus
|
||||||
|
updateMenus(newMenu);
|
||||||
|
showModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeChild = (type, data) => {
|
||||||
|
// console.log(data);
|
||||||
|
// console.log(type);
|
||||||
|
let newMenu = props.menus;
|
||||||
|
if (type == "menu") {
|
||||||
|
let parentMenu = props.parentMenu;
|
||||||
|
|
||||||
|
// Overwrite the props menus
|
||||||
|
newMenu = props.menus.filter((menu) => {
|
||||||
|
return menu.path == data;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove the newMenu from the parentMenu child
|
||||||
|
parentMenu = parentMenu.filter((menu) => {
|
||||||
|
// Level 1
|
||||||
|
if (menu.child) {
|
||||||
|
menu.child.forEach((el) => {
|
||||||
|
// Level 2
|
||||||
|
if (el.child) {
|
||||||
|
el.child = el.child.filter((child) => {
|
||||||
|
return child.path != data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.path == data) {
|
||||||
|
menu.child.splice(menu.child.indexOf(el), 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return menu;
|
||||||
|
});
|
||||||
|
|
||||||
|
newMenu = parentMenu;
|
||||||
|
} else if (type == "header") {
|
||||||
|
// Remove the object array from the props menus
|
||||||
|
newMenu = props.menus.filter((header, index) => {
|
||||||
|
return index != data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the menus
|
||||||
|
updateMenus(newMenu);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<draggable
|
||||||
|
class="dragArea"
|
||||||
|
tag="div"
|
||||||
|
:list="menus"
|
||||||
|
:group="{ name: 'menu', put: props.count == 0 ? false : true }"
|
||||||
|
:clone="clone"
|
||||||
|
item-key="id"
|
||||||
|
>
|
||||||
|
<template #item="{ element }">
|
||||||
|
<rs-card
|
||||||
|
class="p-4 !my-4 mx-0 mb-0 relative border-2 border-[rgb(var(--border-color))]"
|
||||||
|
:class="{
|
||||||
|
'py-6': count > 0,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="text-left font-normal text-xs mb-2">
|
||||||
|
<span class="uppercase text-primary dark:text-primary">{{
|
||||||
|
count == 0 && element.header
|
||||||
|
? element.header
|
||||||
|
: count === 0
|
||||||
|
? "(No Header)"
|
||||||
|
: ""
|
||||||
|
}}</span>
|
||||||
|
<p class="text-gray-500 dark:text-gray-500">
|
||||||
|
{{
|
||||||
|
count == 0 && element.description
|
||||||
|
? element.description
|
||||||
|
: count === 0
|
||||||
|
? "There will be no header shown"
|
||||||
|
: ""
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="count == 0">
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:edit-outline-rounded"
|
||||||
|
class="text-primary hover:text-primary/90 cursor-pointer"
|
||||||
|
size="20"
|
||||||
|
@click="
|
||||||
|
type = 'header';
|
||||||
|
assignDataHeader(element);
|
||||||
|
openModal();
|
||||||
|
"
|
||||||
|
></Icon>
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:close-rounded"
|
||||||
|
class="text-primary hover:text-primary/90 cursor-pointer"
|
||||||
|
size="20"
|
||||||
|
@click="removeChild('header', menus.indexOf(element))"
|
||||||
|
></Icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<p class="flex items-center gap-2">
|
||||||
|
<Icon v-if="element.icon" :name="element.icon" size="22"></Icon>
|
||||||
|
{{ element.title }}
|
||||||
|
</p>
|
||||||
|
<div v-if="count > 0">
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:edit-outline-rounded"
|
||||||
|
class="text-primary hover:text-primary/90 cursor-pointer"
|
||||||
|
size="20"
|
||||||
|
@click="
|
||||||
|
type = 'menu';
|
||||||
|
assignDataMenu(element);
|
||||||
|
openModal();
|
||||||
|
"
|
||||||
|
></Icon>
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:close-rounded"
|
||||||
|
class="text-primary hover:text-primary/90 cursor-pointer"
|
||||||
|
size="20"
|
||||||
|
@click="removeChild('menu', element.path)"
|
||||||
|
></Icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="element?.meta?.auth" class="authuser-wrapper mt-3">
|
||||||
|
<div class="flex">
|
||||||
|
<div v-for="(val, index) in element.meta.auth.user">
|
||||||
|
<rs-badge
|
||||||
|
v-if="index < 5"
|
||||||
|
variant="danger"
|
||||||
|
class="mr-1 text-sm"
|
||||||
|
>
|
||||||
|
{{ val }}
|
||||||
|
</rs-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
<div v-for="(val, index) in element.meta.auth.role">
|
||||||
|
<rs-badge
|
||||||
|
v-if="index < 5"
|
||||||
|
variant="warning"
|
||||||
|
class="mr-1 text-sm"
|
||||||
|
>
|
||||||
|
{{ val }}
|
||||||
|
</rs-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DraggableSideMenuNested
|
||||||
|
:menus="element?.child ? element.child : []"
|
||||||
|
:count="count + 1"
|
||||||
|
:parentMenu="
|
||||||
|
props.parentMenu && props.parentMenu.length > 0
|
||||||
|
? props.parentMenu
|
||||||
|
: props.menus
|
||||||
|
"
|
||||||
|
@changeSideMenu="updateMenus"
|
||||||
|
/>
|
||||||
|
</rs-card>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
|
||||||
|
<rs-modal
|
||||||
|
:title="type == 'header' ? 'Edit Header' : 'Edit Menu'"
|
||||||
|
v-model="showModal"
|
||||||
|
ok-title="Confirm"
|
||||||
|
:ok-callback="saveEditChanges"
|
||||||
|
:cancel-callback="clickCancel"
|
||||||
|
>
|
||||||
|
<div v-if="type == 'header'">
|
||||||
|
<FormKit
|
||||||
|
type="hidden"
|
||||||
|
label="Index"
|
||||||
|
v-model="formHeader.index"
|
||||||
|
></FormKit>
|
||||||
|
<FormKit type="text" label="Name" v-model="formHeader.header"></FormKit>
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Description"
|
||||||
|
v-model="formHeader.description"
|
||||||
|
></FormKit>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="type == 'menu'">
|
||||||
|
<FormKit type="text" label="Title" v-model="formMenu.title"></FormKit>
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Path"
|
||||||
|
v-model="formMenu.path"
|
||||||
|
readonly
|
||||||
|
></FormKit>
|
||||||
|
<FormKit type="text" label="Icon" v-model="formMenu.icon"></FormKit>
|
||||||
|
<div class="mb-4 text-sm">
|
||||||
|
<p class="font-semibold mb-2">
|
||||||
|
Preview Icon (<a
|
||||||
|
href="https://icones.js.org/collection/all"
|
||||||
|
class="text-primary hover:underline"
|
||||||
|
target="_blank"
|
||||||
|
>https://icones.js.org/collection/all</a
|
||||||
|
>)
|
||||||
|
</p>
|
||||||
|
<Icon v-if="formMenu.icon" :name="formMenu.icon"></Icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="mb-4" />
|
||||||
|
<h4 class="text-semibold mb-4">Menu Permission</h4>
|
||||||
|
<FormKit
|
||||||
|
type="radio"
|
||||||
|
label="View Type"
|
||||||
|
v-model="viewPermissionTypeRadio"
|
||||||
|
:classes="{
|
||||||
|
fieldset: 'border-0 !p-0',
|
||||||
|
legend: '!font-semibold !text-sm mb-0',
|
||||||
|
options: '!flex !flex-row gap-4 mt-3',
|
||||||
|
}"
|
||||||
|
:options="viewPermissionType"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="viewPermissionTypeRadio && viewPermissionTypeRadio != 'all'"
|
||||||
|
class="form-wrapper"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<label
|
||||||
|
class="formkit-label flex items-center gap-x-4 font-semibold text-gray-700 dark:text-gray-200 blockfont-semibold text-sm formkit-invalid:text-red-500 dark:formkit-invalid:text-danger"
|
||||||
|
for="input_4"
|
||||||
|
>
|
||||||
|
{{ viewPermissionTypeRadio == "user" ? "User" : "Role" }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<v-select
|
||||||
|
class="formkit-vselect"
|
||||||
|
:options="
|
||||||
|
viewPermissionTypeRadio == 'user'
|
||||||
|
? userList
|
||||||
|
: viewPermissionTypeRadio == 'role'
|
||||||
|
? roleList
|
||||||
|
: []
|
||||||
|
"
|
||||||
|
v-model="selectListValue"
|
||||||
|
multiple
|
||||||
|
></v-select>
|
||||||
|
<FormKit
|
||||||
|
type="checkbox"
|
||||||
|
v-model="checkAll"
|
||||||
|
:label="
|
||||||
|
viewPermissionTypeRadio == 'user'
|
||||||
|
? 'Check All User'
|
||||||
|
: 'Check All Role'
|
||||||
|
"
|
||||||
|
input-class="icon-check"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</rs-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
13
components/formkit/DateTimePicker.vue
Normal file
13
components/formkit/DateTimePicker.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
context: Object,
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleInput(e) {
|
||||||
|
props.context.node.input(e.target.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input @input="handleInput" :value="props.context._value" />
|
||||||
|
</template>
|
||||||
139
components/formkit/FileDropzone.vue
Normal file
139
components/formkit/FileDropzone.vue
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<script setup>
|
||||||
|
/* eslint-disable */
|
||||||
|
import { useDropzone } from "vue3-dropzone";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
context: Object,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileBase64 = ref([]);
|
||||||
|
const files = ref([]);
|
||||||
|
let err = ref(false);
|
||||||
|
let errmsg = ref("");
|
||||||
|
|
||||||
|
const accept = props.context.accept;
|
||||||
|
const multiple = props.context.multiple;
|
||||||
|
const maxSize = props.context.maxSize;
|
||||||
|
const minSize = props.context.minSize;
|
||||||
|
const maxFiles = props.context.maxFiles;
|
||||||
|
const disabled = props.context.disabled;
|
||||||
|
|
||||||
|
const toBase64 = (file) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => resolve(reader.result);
|
||||||
|
reader.onerror = (error) => reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onDrop(fileList, fileError, event) {
|
||||||
|
if (fileError.length == 0) {
|
||||||
|
err.value = false;
|
||||||
|
errmsg.value = "";
|
||||||
|
for (let i = 0; i < fileList.length; i++) {
|
||||||
|
const base64 = await toBase64(fileList[i]);
|
||||||
|
fileBase64.value.push({ data: fileList[i], base64 });
|
||||||
|
files.value.push([fileList[i]]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err.value = true;
|
||||||
|
errmsg.value = fileError[0].errors[0].message;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNodeValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeFiles(index) {
|
||||||
|
fileBase64.value.splice(index, 1);
|
||||||
|
files.value.splice(index, 1);
|
||||||
|
updateNodeValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNodeValue() {
|
||||||
|
props.context.node.input(files.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
|
onDrop,
|
||||||
|
accept,
|
||||||
|
multiple: multiple === "true" ? true : false,
|
||||||
|
maxSize: maxSize ? parseInt(maxSize) : Infinity,
|
||||||
|
minSize: minSize ? parseInt(minSize) : 0,
|
||||||
|
maxFiles: maxFiles ? parseInt(maxFiles) : 0,
|
||||||
|
disabled: disabled === "true" ? true : false,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- eslint-disable -->
|
||||||
|
|
||||||
|
<div :class="context.classes.dropzone">
|
||||||
|
<div v-bind="getRootProps()" class="cursor-pointer">
|
||||||
|
<input v-bind="getInputProps()" />
|
||||||
|
<div class="flex items-center justify-center h-36">
|
||||||
|
<div>
|
||||||
|
<Icon
|
||||||
|
class="!block m-auto mb-3"
|
||||||
|
size="30px"
|
||||||
|
name="ic:outline-upload-file"
|
||||||
|
/>
|
||||||
|
<p class="text-center" v-if="isDragActive">Drop the files here ...</p>
|
||||||
|
<p v-else>Drop files or click here to upload files</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="fileList"
|
||||||
|
class="grid sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4"
|
||||||
|
v-auto-animate
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(file, index) in fileBase64"
|
||||||
|
class="relative overflow-hidden w-full h-20 md:h-36 rounded-lg border-2 border-[rgb(var(--border-color))]"
|
||||||
|
v-auto-animate
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="file.data.type.includes('image')"
|
||||||
|
:src="file.base64"
|
||||||
|
class="w-full h-20 md:h-36 object-cover object-center rounded-lg"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="h-full flex items-center justify-center opacity-50 text-primary font-semibold uppercase text-xl whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
file.data.name.slice(
|
||||||
|
((file.data.name.lastIndexOf(".") - 1) >>> 0) + 2
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<Icon
|
||||||
|
name="ic:round-close"
|
||||||
|
@click="removeFiles(index)"
|
||||||
|
class="cursor-pointer absolute top-1 right-1 text-[rgb(var(--text-color))] bg-[rgb(var(--bg-2))] p-1 rounded-full"
|
||||||
|
size="18"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute bottom-1 right-1 bg-[rgb(var(--bg-2))] px-2 rounded-lg"
|
||||||
|
>
|
||||||
|
<span class="font-semibold text-xs text-[rgb(var(--text-color))]">
|
||||||
|
{{ file.data.path }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
v-if="err"
|
||||||
|
class="formkit-messages list-none p-0 mt-1 mb-0 relative -bottom-5 -left-2"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="formkit-message text-red-500 mb-1 text-xs formkit-invalid:text-red-500 dark:formkit-invalid:text-danger"
|
||||||
|
id="input_9-rule_required"
|
||||||
|
data-message-type="validation"
|
||||||
|
>
|
||||||
|
{{ errmsg }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
83
components/formkit/OneTimePassword.vue
Normal file
83
components/formkit/OneTimePassword.vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<script setup>
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
context: Object,
|
||||||
|
});
|
||||||
|
|
||||||
|
const digits = Number(props.context.digits);
|
||||||
|
const tmp = ref(props.context.value || "");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle input, advancing or retreating focus.
|
||||||
|
*/
|
||||||
|
function handleInput(index, e) {
|
||||||
|
const prev = tmp.value;
|
||||||
|
|
||||||
|
if (tmp.value.length <= index) {
|
||||||
|
// If this is a new digit
|
||||||
|
tmp.value = "" + tmp.value + e.target.value;
|
||||||
|
} else {
|
||||||
|
// If this digit is in the middle somewhere, cut the string into two
|
||||||
|
// pieces at the index, and insert our new digit in.
|
||||||
|
tmp.value =
|
||||||
|
"" +
|
||||||
|
tmp.value.substr(0, index) +
|
||||||
|
e.target.value +
|
||||||
|
tmp.value.substr(index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all the digit inputs
|
||||||
|
const inputs = e.target.parentElement.querySelectorAll("input");
|
||||||
|
|
||||||
|
if (index < digits - 1 && tmp.value.length >= prev.length) {
|
||||||
|
// If this is a new input and not at the end, focus the next input
|
||||||
|
inputs.item(index + 1).focus();
|
||||||
|
} else if (index > 0 && tmp.value.length < prev.length) {
|
||||||
|
// in this case we deleted a value, focus backwards
|
||||||
|
inputs.item(index - 1).focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tmp.value.length === digits) {
|
||||||
|
// If our input is complete, commit the value.
|
||||||
|
props.context.node.input(tmp.value);
|
||||||
|
} else if (tmp.value.length < digits && props.context.value !== "") {
|
||||||
|
// If our input is incomplete, it should have no value.
|
||||||
|
props.context.node.input("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On focus, select the text in our input.
|
||||||
|
*/
|
||||||
|
function handleFocus(e) {
|
||||||
|
e.target.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the paste event.
|
||||||
|
*/
|
||||||
|
function handlePaste(e) {
|
||||||
|
const paste = e.clipboardData.getData("text");
|
||||||
|
if (typeof paste === "string") {
|
||||||
|
// If it is the right length, paste it.
|
||||||
|
tmp.value = paste.substr(0, digits);
|
||||||
|
const inputs = e.target.parentElement.querySelectorAll("input");
|
||||||
|
// Focus on the last character
|
||||||
|
inputs.item(tmp.value.length - 1).focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- eslint-disable -->
|
||||||
|
<input
|
||||||
|
v-for="index in digits"
|
||||||
|
maxlength="1"
|
||||||
|
:class="context.classes.digit"
|
||||||
|
:value="tmp[index - 1] || ''"
|
||||||
|
@input="handleInput(index - 1, $event)"
|
||||||
|
@focus="handleFocus"
|
||||||
|
@paste="handlePaste"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
23
components/formkit/TextMask.vue
Normal file
23
components/formkit/TextMask.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup>
|
||||||
|
/* eslint-disable */
|
||||||
|
const props = defineProps({
|
||||||
|
context: Object,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mask = String(props.context.mask);
|
||||||
|
// console.log(props.context);
|
||||||
|
|
||||||
|
function handleInput(e) {
|
||||||
|
props.context.node.input(e.target.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input
|
||||||
|
@input="handleInput"
|
||||||
|
:class="context.classes.input"
|
||||||
|
:value="props.context._value"
|
||||||
|
:placeholder="props.context.attrs.placeholder"
|
||||||
|
v-maska="mask"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
36
components/formkit/Toggle.vue
Normal file
36
components/formkit/Toggle.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
context: Object,
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleChange(event) {
|
||||||
|
props.context.node.input(event.target.checked);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<label
|
||||||
|
:class="context.classes.toggle"
|
||||||
|
class="inline-flex items-center mb-5 cursor-pointer mt-1"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="context.value"
|
||||||
|
:disabled="context.disabled"
|
||||||
|
class="sr-only peer"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:w-5 after:h-5 after:transition-all peer-checked:bg-blue-600"
|
||||||
|
></div>
|
||||||
|
<span class="ms-3 text-sm font-medium text-gray-900">
|
||||||
|
{{
|
||||||
|
context.onLabel || context.offLabel
|
||||||
|
? context.value
|
||||||
|
? context.onLabel
|
||||||
|
: context.offLabel
|
||||||
|
: context.label
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
88
components/layouts/Breadcrumb.vue
Normal file
88
components/layouts/Breadcrumb.vue
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<script setup>
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
// Get breadcrumb from page meta
|
||||||
|
const breadcrumb = computed(() => {
|
||||||
|
let breadcrumb = null;
|
||||||
|
const matched = route.matched;
|
||||||
|
|
||||||
|
if (matched[matched.length - 1].meta?.breadcrumb) {
|
||||||
|
breadcrumb = matched[matched.length - 1].meta.breadcrumb;
|
||||||
|
} else {
|
||||||
|
// if no breadcrumb in page meta, get breadcrumb from route matched
|
||||||
|
breadcrumb = matched.map((item) => {
|
||||||
|
return {
|
||||||
|
name: item.name,
|
||||||
|
path: item.path,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return breadcrumb;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if type current overwrite path to its own path
|
||||||
|
if (breadcrumb) {
|
||||||
|
breadcrumb.forEach((item) => {
|
||||||
|
if (item.type == "current") {
|
||||||
|
item.path = route.path;
|
||||||
|
} else if (item.type == "parent") {
|
||||||
|
item.path = route.path.split("/").slice(0, -item.parentNo).join("/");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return breadcrumb;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get title from page meta
|
||||||
|
const title = computed(() => {
|
||||||
|
const matched = route.matched;
|
||||||
|
const title = matched[matched.length - 1].name;
|
||||||
|
return title;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function navigateMenu(path) {
|
||||||
|
try {
|
||||||
|
await navigateTo(path);
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="breadcrumb" class="mb-6">
|
||||||
|
<nav aria-label="Breadcrumb" class="mb-4">
|
||||||
|
<ol class="flex items-center text-sm">
|
||||||
|
<li class="flex items-center">
|
||||||
|
<NuxtLink to="/" class="text-gray-500 hover:text-gray-700">
|
||||||
|
<Icon name="mdi:home" size="16" />
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
<li v-for="(item, index) in breadcrumb" :key="index" class="flex items-center">
|
||||||
|
<Icon
|
||||||
|
name="mdi:chevron-right"
|
||||||
|
size="16"
|
||||||
|
class="mx-2 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
@click="navigateMenu(item.path)"
|
||||||
|
class="cursor-pointer capitalize"
|
||||||
|
:class="{
|
||||||
|
'text-gray-500 hover:text-gray-700': index !== breadcrumb.length - 1,
|
||||||
|
'text-primary font-medium': index === breadcrumb.length - 1,
|
||||||
|
}"
|
||||||
|
:aria-current="index === breadcrumb.length - 1 ? 'page' : undefined"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- <div class="flex justify-between items-center">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-800">{{ title }}</h1>
|
||||||
|
<slot name="right"></slot>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
98
components/layouts/BreadcrumbV2.vue
Normal file
98
components/layouts/BreadcrumbV2.vue
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<script setup>
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
// Get breadcrumb from page meta
|
||||||
|
const breadcrumb = computed(() => {
|
||||||
|
let breadcrumb = null;
|
||||||
|
const matched = route.matched;
|
||||||
|
|
||||||
|
if (matched[matched.length - 1].meta?.breadcrumb) {
|
||||||
|
breadcrumb = matched[matched.length - 1].meta.breadcrumb;
|
||||||
|
} else {
|
||||||
|
// if no breadcrumb in page meta, get breadcrumb from route matched
|
||||||
|
breadcrumb = matched.map((item) => {
|
||||||
|
return {
|
||||||
|
name: item.meta.title,
|
||||||
|
path: item.path,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return breadcrumb;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if type current overwrite path to its own path
|
||||||
|
if (breadcrumb) {
|
||||||
|
breadcrumb.forEach((item) => {
|
||||||
|
if (item.type == "current") {
|
||||||
|
item.path = route.path;
|
||||||
|
} else if (item.type == "parent") {
|
||||||
|
item.path = route.path.split("/").slice(0, -item.parentNo).join("/");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return breadcrumb;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get title from page meta
|
||||||
|
const title = computed(() => {
|
||||||
|
const matched = route.matched;
|
||||||
|
const title = matched[matched.length - 1].meta.title;
|
||||||
|
return title;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function navigateMenu(path) {
|
||||||
|
try {
|
||||||
|
await navigateTo(path);
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="breadcrumb">
|
||||||
|
<div class="flex flex-col md:flex-row items-stretch justify-between">
|
||||||
|
<div
|
||||||
|
class="flex items-center"
|
||||||
|
v-if="breadcrumb && breadcrumb.length != 0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="(item, index) in breadcrumb"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center text-primary"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
v-if="index != 0"
|
||||||
|
name="ic:round-chevron-right"
|
||||||
|
size="14"
|
||||||
|
class="mr-1 text-gray-500"
|
||||||
|
></Icon>
|
||||||
|
<a
|
||||||
|
@click="navigateMenu(item.path)"
|
||||||
|
class="cursor-pointer hover:underline pr-1 font-normal text-sm text-gray-500"
|
||||||
|
:class="{
|
||||||
|
'!text-primary': breadcrumb.length - 1 == index,
|
||||||
|
}"
|
||||||
|
>{{ item.name }}</a
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<slot name="right"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative flex flex-col min-w-0 lg:mb-0 break-words w-full">
|
||||||
|
<div class="rounded-t mb-5">
|
||||||
|
<div class="flex flex-wrap items-center">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xl font-semibold text-gray-600">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
20
components/layouts/FormHeader.vue
Normal file
20
components/layouts/FormHeader.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script setup></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="w-full h-14 z-20 bg-white dark:bg-slate-800 fixed top-0 right-0 px-5 py-3 duration-300 shadow-md shadow-slate-200 dark:shadow-slate-900"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-800 dark:text-white">
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:chevron-left-rounded"
|
||||||
|
class="inline-block w-6 h-6 mr-2"
|
||||||
|
/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
327
components/layouts/Header.vue
Normal file
327
components/layouts/Header.vue
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
<script setup>
|
||||||
|
const isVertical = ref(true);
|
||||||
|
const isDesktop = ref(true);
|
||||||
|
|
||||||
|
const emit = defineEmits(["toggleMenu"]);
|
||||||
|
|
||||||
|
// Use site settings composable
|
||||||
|
const { siteSettings, setTheme, getCurrentTheme } = useSiteSettings();
|
||||||
|
|
||||||
|
// const { locale } = useI18n();
|
||||||
|
// const colorMode = useColorMode();
|
||||||
|
const langList = languageList();
|
||||||
|
|
||||||
|
const locale = ref("en");
|
||||||
|
|
||||||
|
const themes = themeList();
|
||||||
|
const themes2 = themeList2();
|
||||||
|
|
||||||
|
function setThemeLocal(theme) {
|
||||||
|
setTheme(theme); // Use the site settings setTheme function
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbToHex(rgbString) {
|
||||||
|
// Split the input string into an array of components
|
||||||
|
const rgbArray = rgbString.split(",");
|
||||||
|
|
||||||
|
// Convert each component to its numeric value
|
||||||
|
const r = parseInt(rgbArray[0].trim(), 10);
|
||||||
|
const g = parseInt(rgbArray[1].trim(), 10);
|
||||||
|
const b = parseInt(rgbArray[2].trim(), 10);
|
||||||
|
|
||||||
|
// Convert the numeric RGB values to hexadecimal
|
||||||
|
const rHex = r.toString(16).padStart(2, "0");
|
||||||
|
const gHex = g.toString(16).padStart(2, "0");
|
||||||
|
const bHex = b.toString(16).padStart(2, "0");
|
||||||
|
|
||||||
|
// Concatenate the components and return the final hexadecimal color code
|
||||||
|
return `#${rHex}${gHex}${bHex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle Open/Close menu
|
||||||
|
const toggleMenu = (event) => {
|
||||||
|
emit("toggleMenu", event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Focus on search input
|
||||||
|
function toggleSearch() {
|
||||||
|
document.getElementById("header-search").value = "";
|
||||||
|
document.getElementById("header-search").focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change language
|
||||||
|
const changeLanguage = (lang) => {
|
||||||
|
locale.value = lang;
|
||||||
|
};
|
||||||
|
|
||||||
|
const languageNow = computed(() => {
|
||||||
|
return langList.find((lang) => lang.value == locale.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get current theme icon
|
||||||
|
const currentThemeIcon = computed(() => {
|
||||||
|
const theme = getCurrentTheme();
|
||||||
|
return theme === "dark" ? "ic:outline-dark-mode" : "ic:outline-light-mode";
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// If mobile toggleMenu
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
emit("toggleMenu", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load site settings on mount and ensure they're properly populated
|
||||||
|
const { loadSiteSettings } = useSiteSettings();
|
||||||
|
loadSiteSettings().then(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
// console.log('[Header.vue] Site settings loaded. Name:', siteSettings.value?.siteName, 'ShowInHeader:', siteSettings.value?.showSiteNameInHeader, 'Logo:', siteSettings.value?.siteLogo);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add computed to ensure logo reactivity
|
||||||
|
const currentLogo = computed(() => {
|
||||||
|
const logoUrl = siteSettings.value?.siteLogo;
|
||||||
|
if (logoUrl && logoUrl.trim() !== "") {
|
||||||
|
return logoUrl; // Use logo from settings if available and not empty
|
||||||
|
}
|
||||||
|
return "/img/logo/corradAF-logo.svg"; // Ultimate fallback
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-header">
|
||||||
|
<div class="flex items-stretch justify-between">
|
||||||
|
<div v-if="isVertical" class="flex">
|
||||||
|
<span class="flex items-center justify-center">
|
||||||
|
<button class="icon-btn h-10 w-10 rounded-full" @click="toggleMenu">
|
||||||
|
<Icon name="ic:round-menu" class="" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<!-- Site logo and name for vertical layout - only show if explicitly enabled -->
|
||||||
|
<div
|
||||||
|
v-if="siteSettings?.value?.showSiteNameInHeader"
|
||||||
|
class="flex items-center ml-4"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="currentLogo"
|
||||||
|
:alt="siteSettings?.value?.siteName || 'Site Logo'"
|
||||||
|
class="h-8 block"
|
||||||
|
@error="$event.target.src = '/img/logo/corradAF-logo.svg'"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="siteSettings?.value?.siteName"
|
||||||
|
class="text-lg font-semibold"
|
||||||
|
:class="{ 'ml-3': siteSettings?.value?.siteLogo }"
|
||||||
|
:style="{
|
||||||
|
fontSize: (siteSettings?.value?.siteNameFontSize || 18) + 'px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ siteSettings?.value?.siteName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex" v-else>
|
||||||
|
<nuxt-link to="/">
|
||||||
|
<div class="flex flex-auto gap-3 justify-center items-center">
|
||||||
|
<img
|
||||||
|
:src="currentLogo"
|
||||||
|
:alt="siteSettings?.value?.siteName || 'Site Logo'"
|
||||||
|
class="h-8 block"
|
||||||
|
@error="$event.target.src = '/img/logo/corradAF-logo.svg'"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
siteSettings?.value?.siteName && siteSettings?.value?.showSiteNameInHeader
|
||||||
|
"
|
||||||
|
class="text-lg font-semibold"
|
||||||
|
:style="{
|
||||||
|
fontSize: (siteSettings?.value?.siteNameFontSize || 18) + 'px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ siteSettings?.value?.siteName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 item-center justify-items-end">
|
||||||
|
<VoiceReader class="ml-4" />
|
||||||
|
|
||||||
|
<!-- New dropdown for themeList.js -->
|
||||||
|
<VDropdown placement="bottom-end" distance="13" name="theme">
|
||||||
|
<button class="icon-btn h-10 w-10 rounded-full">
|
||||||
|
<Icon size="22px" name="ph:paint-brush-broad" />
|
||||||
|
</button>
|
||||||
|
<template #popper>
|
||||||
|
<ul class="header-dropdown w-full md:w-52">
|
||||||
|
<li v-for="(val, index) in themes" :key="index">
|
||||||
|
<a
|
||||||
|
@click="setThemeLocal(val.theme)"
|
||||||
|
class="flex justify-between items-center cursor-pointer py-2 px-4 hover:bg-[rgb(var(--bg-1))]"
|
||||||
|
>
|
||||||
|
<span class="capitalize"> {{ val.theme }} </span>
|
||||||
|
<div class="flex items-center gap-x-1">
|
||||||
|
<div
|
||||||
|
v-for="(color, colorIndex) in val.colors"
|
||||||
|
:key="colorIndex"
|
||||||
|
class="h-[25px] w-[10px] rounded-lg"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: rgbToHex(color.value),
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
|
||||||
|
<!-- New dropdown for themeList2.js -->
|
||||||
|
<VDropdown placement="bottom-end" distance="13" name="theme2">
|
||||||
|
<button class="icon-btn h-10 w-10 rounded-full">
|
||||||
|
<Icon size="22px" name="ph:wheelchair" />
|
||||||
|
</button>
|
||||||
|
<template #popper>
|
||||||
|
<ul class="header-dropdown w-full md:w-52">
|
||||||
|
<li v-for="(val, index) in themes2" :key="index">
|
||||||
|
<a
|
||||||
|
@click="setThemeLocal(val.theme)"
|
||||||
|
class="flex justify-between items-center cursor-pointer py-2 px-4 hover:bg-[rgb(var(--bg-1))]"
|
||||||
|
>
|
||||||
|
<span class="capitalize"> {{ val.theme }} </span>
|
||||||
|
<div class="flex items-center gap-x-1">
|
||||||
|
<div
|
||||||
|
v-for="(color, colorIndex) in val.colors"
|
||||||
|
:key="colorIndex"
|
||||||
|
class="h-[25px] w-[10px] rounded-lg"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: rgbToHex(color.value),
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
|
||||||
|
<VDropdown placement="bottom-end" distance="13" name="notification">
|
||||||
|
<button class="relative icon-btn h-10 w-10 rounded-full">
|
||||||
|
<span class="w-3 h-3 absolute top-1 right-2 rounded-full bg-primary"></span>
|
||||||
|
<Icon name="ic:round-notifications-none" class="" />
|
||||||
|
</button>
|
||||||
|
<template #popper>
|
||||||
|
<ul class="header-dropdown w-full md:w-80 text-[#4B5563]">
|
||||||
|
<li class="d-head flex items-center justify-between py-2 px-4">
|
||||||
|
<span class="font-semibold">Notification</span>
|
||||||
|
<div
|
||||||
|
class="flex items-center text-primary cursor-pointer hover:underline"
|
||||||
|
>
|
||||||
|
<a class="ml-2">View All</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<NuxtScrollbar>
|
||||||
|
<li>
|
||||||
|
<div class="bg-[rgb(var(--bg-1))] py-2 px-4">Today</div>
|
||||||
|
<a class="py-2 px-4 block">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-circle" class="text-primary flex-none" />
|
||||||
|
<span class="mx-2"
|
||||||
|
>Terdapat Satu Pembayaran yang berlaku menggunakan bil Kuih Raya
|
||||||
|
Cik Kiah</span
|
||||||
|
>
|
||||||
|
<div class="w-12 h-12 rounded-full ml-auto flex-none">
|
||||||
|
<img class="rounded-full" src="@/assets/img/user/default.svg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="py-2 px-4 block">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-circle" class="text-primary flex-none" />
|
||||||
|
<span class="mx-2"
|
||||||
|
>Terdapat Satu Pembayaran yang berlaku menggunakan bil
|
||||||
|
Mercun</span
|
||||||
|
>
|
||||||
|
<div class="w-12 h-12 rounded-full ml-auto flex-none">
|
||||||
|
<img
|
||||||
|
class="rounded-full"
|
||||||
|
src="@/assets/img/user/default.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</NuxtScrollbar>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
|
||||||
|
<VDropdown
|
||||||
|
placement="bottom-end"
|
||||||
|
distance="13"
|
||||||
|
name="profile"
|
||||||
|
class="flex justify-center item-center"
|
||||||
|
>
|
||||||
|
<button class="icon-btn profile px-2">
|
||||||
|
<img
|
||||||
|
class="w-8 h-8 object-cover rounded-full"
|
||||||
|
src="@/assets/img/user/default.svg"
|
||||||
|
/>
|
||||||
|
<div v-if="isDesktop" class="grid grid-cols-1 text-left ml-3 flex-none">
|
||||||
|
<p class="font-semibold text-sm truncate w-24 mb-0">Johan</p>
|
||||||
|
</div>
|
||||||
|
<Icon name="ic:outline-keyboard-arrow-down" class="ml-3" />
|
||||||
|
</button>
|
||||||
|
<template #popper>
|
||||||
|
<ul class="header-dropdown w-full md:w-52">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="/logout"
|
||||||
|
class="flex items-center cursor-pointer py-2 px-4 hover:bg-[rgb(var(--bg-1))]"
|
||||||
|
>
|
||||||
|
<Icon name="ic:outline-logout" class="mr-2" />
|
||||||
|
Logout
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Nav for Layout Vertical -->
|
||||||
|
<div tabindex="0" class="w-header-search">
|
||||||
|
<Icon name="ic:outline-search" class="mr-3" />
|
||||||
|
<FormKit
|
||||||
|
id="header-search"
|
||||||
|
:classes="{
|
||||||
|
outer: 'mb-0 flex-1',
|
||||||
|
}"
|
||||||
|
type="search"
|
||||||
|
placeholder="Search..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.popper) {
|
||||||
|
background: #e92791;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.popper #arrow::before) {
|
||||||
|
background: #e92791;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.popper:hover),
|
||||||
|
:deep(.popper:hover > #arrow::before) {
|
||||||
|
background: #e92791;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
241
components/layouts/configmenu/index.vue
Normal file
241
components/layouts/configmenu/index.vue
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useThemeStore } from "~/stores/theme";
|
||||||
|
|
||||||
|
const colorMode = useColorMode();
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
|
const hideConfigMenu = ref(true);
|
||||||
|
const rgbColor = ref({});
|
||||||
|
|
||||||
|
const hexToRgb = (hex) => {
|
||||||
|
const r = parseInt(hex.slice(1, 3), 16);
|
||||||
|
const g = parseInt(hex.slice(3, 5), 16);
|
||||||
|
const b = parseInt(hex.slice(5, 7), 16);
|
||||||
|
return `${r}, ${g}, ${b}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setThemeColor = (type, color) => {
|
||||||
|
themeStore.setThemeColor(type, color);
|
||||||
|
|
||||||
|
// Remove Color form type string
|
||||||
|
const colorType = type.replace("Color", "").toLowerCase();
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
`--color-${colorType}`,
|
||||||
|
hexToRgb(color)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function setColorMode() {
|
||||||
|
if (colorMode.preference == "light") {
|
||||||
|
colorMode.preference = "dark";
|
||||||
|
|
||||||
|
document.documentElement.classList.remove("light");
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
} else {
|
||||||
|
colorMode.preference = "light";
|
||||||
|
|
||||||
|
document.documentElement.classList.remove("dark");
|
||||||
|
document.documentElement.classList.add("light");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setThemeGlobal = () => {
|
||||||
|
rgbColor.value.primary = themeStore.primaryColor;
|
||||||
|
rgbColor.value.secondary = themeStore.secondaryColor;
|
||||||
|
rgbColor.value.info = themeStore.infoColor;
|
||||||
|
rgbColor.value.success = themeStore.successColor;
|
||||||
|
rgbColor.value.warning = themeStore.warningColor;
|
||||||
|
rgbColor.value.danger = themeStore.dangerColor;
|
||||||
|
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
"--color-primary",
|
||||||
|
hexToRgb(themeStore.primaryColor)
|
||||||
|
);
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
"--color-secondary",
|
||||||
|
hexToRgb(themeStore.secondaryColor)
|
||||||
|
);
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
"--color-info",
|
||||||
|
hexToRgb(themeStore.infoColor)
|
||||||
|
);
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
"--color-success",
|
||||||
|
hexToRgb(themeStore.successColor)
|
||||||
|
);
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
"--color-warning",
|
||||||
|
hexToRgb(themeStore.warningColor)
|
||||||
|
);
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
"--color-danger",
|
||||||
|
hexToRgb(themeStore.dangerColor)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setThemeGlobal();
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetTheme = () => {
|
||||||
|
themeStore.resetThemeColor();
|
||||||
|
setThemeGlobal();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
:class="{
|
||||||
|
'right-[-300px]': hideConfigMenu,
|
||||||
|
}"
|
||||||
|
class="h-full w-[300px] bg-white dark:bg-slate-800 fixed top-0 right-0 z-20 shadow-md shadow-slate-200 dark:shadow-slate-900 duration-300"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
@click="hideConfigMenu = !hideConfigMenu"
|
||||||
|
class="py-3 px-4 bg-primary text-white rounded-l-lg absolute bottom-1 left-[-53px] cursor-pointer hover:bg-primary/90 hidden md:block"
|
||||||
|
>
|
||||||
|
<Icon name="fluent:paint-brush-16-regular" size="22px" />
|
||||||
|
</div>
|
||||||
|
<div class="p-4 flex font-normal border-b">
|
||||||
|
<p class="flex-1">
|
||||||
|
<span class="font-bold text-base text-gray-600 dark:text-white"
|
||||||
|
>Theme Customizer</span
|
||||||
|
>
|
||||||
|
<br />
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-300">
|
||||||
|
Customize Theme Real Time
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<Icon
|
||||||
|
@click="hideConfigMenu = !hideConfigMenu"
|
||||||
|
class="cursor-pointer"
|
||||||
|
name="material-symbols:close-rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<p class="text-sm mb-3 font-bold">Color Mode</p>
|
||||||
|
<FormKit
|
||||||
|
v-model="colorMode.value"
|
||||||
|
type="radio"
|
||||||
|
label="Theme"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
label: 'Light',
|
||||||
|
value: 'light',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Dark',
|
||||||
|
value: 'dark',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:classes="{
|
||||||
|
fieldset: 'border-0 !p-0',
|
||||||
|
legend: '!font-medium !text-sm mb-0',
|
||||||
|
options: '!flex !flex-row gap-4 mt-2',
|
||||||
|
input: '!rounded-full',
|
||||||
|
}"
|
||||||
|
@change="setColorMode"
|
||||||
|
/>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<p class="text-sm mb-3 font-bold">Theming</p>
|
||||||
|
<a
|
||||||
|
@click="resetTheme"
|
||||||
|
class="underline text-blue-600 cursor-pointer hover:text-blue-400"
|
||||||
|
>Reset</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2">
|
||||||
|
<FormKit
|
||||||
|
type="color"
|
||||||
|
v-model="rgbColor.primary"
|
||||||
|
label="Primary"
|
||||||
|
:classes="{
|
||||||
|
label: '!font-medium !text-sm mb-0',
|
||||||
|
outer: '!mb-0',
|
||||||
|
}"
|
||||||
|
@input="setThemeColor('primaryColor', rgbColor.primary)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="color"
|
||||||
|
v-model="rgbColor.secondary"
|
||||||
|
label="Secondary"
|
||||||
|
:classes="{
|
||||||
|
label: '!font-medium !text-sm mb-0',
|
||||||
|
outer: '!mb-0',
|
||||||
|
}"
|
||||||
|
@input="setThemeColor('secondaryColor', rgbColor.secondary)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="color"
|
||||||
|
v-model="rgbColor.info"
|
||||||
|
label="Info"
|
||||||
|
:classes="{
|
||||||
|
label: '!font-medium !text-sm mb-0',
|
||||||
|
outer: '!mb-0',
|
||||||
|
}"
|
||||||
|
@input="setThemeColor('infoColor', rgbColor.info)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="color"
|
||||||
|
v-model="rgbColor.success"
|
||||||
|
label="Success"
|
||||||
|
:classes="{
|
||||||
|
label: '!font-medium !text-sm mb-0',
|
||||||
|
outer: '!mb-0',
|
||||||
|
}"
|
||||||
|
@input="setThemeColor('successColor', rgbColor.success)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="color"
|
||||||
|
v-model="rgbColor.warning"
|
||||||
|
label="Warning"
|
||||||
|
:classes="{
|
||||||
|
label: '!font-medium !text-sm mb-0',
|
||||||
|
outer: '!mb-0',
|
||||||
|
}"
|
||||||
|
@input="setThemeColor('warningColor', rgbColor.warning)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="color"
|
||||||
|
v-model="rgbColor.danger"
|
||||||
|
label="Danger"
|
||||||
|
:classes="{
|
||||||
|
label: '!font-medium !text-sm mb-0',
|
||||||
|
outer: '!mb-0',
|
||||||
|
}"
|
||||||
|
@input="setThemeColor('dangerColor', rgbColor.danger)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<hr class="my-4" />
|
||||||
|
<p class="text-sm mb-3 font-bold">Preview</p>
|
||||||
|
<rs-button variant="primary" class="w-full mb-4 cursor-default">
|
||||||
|
Primary Color
|
||||||
|
</rs-button>
|
||||||
|
<rs-button variant="secondary" class="w-full mb-4 cursor-default">
|
||||||
|
Secondary Color
|
||||||
|
</rs-button>
|
||||||
|
<rs-button variant="info" class="w-full mb-4 cursor-default">
|
||||||
|
Info Color
|
||||||
|
</rs-button>
|
||||||
|
<rs-button variant="success" class="w-full mb-4 cursor-default">
|
||||||
|
Success Color
|
||||||
|
</rs-button>
|
||||||
|
<rs-button variant="warning" class="w-full mb-4 cursor-default">
|
||||||
|
Warning Color
|
||||||
|
</rs-button>
|
||||||
|
<rs-button variant="danger" class="w-full mb-4 cursor-default">
|
||||||
|
Danger Color
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
7
components/layouts/horizontal/index.vue
Normal file
7
components/layouts/horizontal/index.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
25
components/layouts/index.vue
Normal file
25
components/layouts/index.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useThemeStore } from "~/stores/theme";
|
||||||
|
|
||||||
|
// Import Layout Components
|
||||||
|
import RSVertical from "~/components/layouts/vertical/index.vue";
|
||||||
|
import RSHorizontal from "~/components/layouts/horizontal/index.vue";
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
const layoutType = themeStore.layoutType;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="layout-wrapper">
|
||||||
|
<div v-if="layoutType === 'vertical'" class="v-layout h-100">
|
||||||
|
<RSVertical>
|
||||||
|
<slot />
|
||||||
|
</RSVertical>
|
||||||
|
</div>
|
||||||
|
<div v-if="layoutType === 'horizontal'" class="h-layout h-100">
|
||||||
|
<RSHorizontal>
|
||||||
|
<slot />
|
||||||
|
</RSHorizontal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
166
components/layouts/sidemenu/Item.vue
Normal file
166
components/layouts/sidemenu/Item.vue
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useLayoutStore } from "~/stores/layout";
|
||||||
|
import { useWindowSize } from "vue-window-size";
|
||||||
|
import RSChildItem from "~/components/layouts/sidemenu/ItemChild.vue";
|
||||||
|
import { useUserStore } from "~/stores/user";
|
||||||
|
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
const mobileWidth = layoutStore.mobileWidth;
|
||||||
|
const { width } = useWindowSize();
|
||||||
|
|
||||||
|
const user = useUserStore();
|
||||||
|
const route = useRoute();
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const username = user.username;
|
||||||
|
const roles = user.roles;
|
||||||
|
|
||||||
|
const menuItem = props.items ? props.items : [];
|
||||||
|
|
||||||
|
// validate userExist on meta.auth.user
|
||||||
|
function userExist(item) {
|
||||||
|
if (item.meta?.auth?.user) {
|
||||||
|
if (item.meta?.auth?.user.some((e) => e === username)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate roleExist on meta.auth.role
|
||||||
|
function roleExist(item) {
|
||||||
|
if (item.meta?.auth?.role) {
|
||||||
|
if (item.meta?.auth?.role.some((e) => roles?.includes(e))) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle show and hide menu content
|
||||||
|
function openMenu(event) {
|
||||||
|
const target = event.currentTarget;
|
||||||
|
try {
|
||||||
|
target.querySelector("a").classList.toggle("nav-open");
|
||||||
|
target.querySelector("ul").classList.toggle("hide");
|
||||||
|
} catch (e) {
|
||||||
|
// console.log(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active menu
|
||||||
|
function activeMenu(routePath) {
|
||||||
|
return route.path == routePath
|
||||||
|
? `bg-[rgb(var(--color-primary))] font-normal text-white active-menu`
|
||||||
|
: `font-light text-white/90 md:transition-all md:duration-200 hover:md:ml-2`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMenu() {
|
||||||
|
document.querySelector(".v-layout").classList.toggle("menu-hide");
|
||||||
|
document.getElementsByClassName("menu-overlay")[0].classList.toggle("hide");
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigationPage(path, external) {
|
||||||
|
if (width.value <= mobileWidth) toggleMenu();
|
||||||
|
navigateTo(path, {
|
||||||
|
external: external,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-for="(item, index) in menuItem" :key="index">
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
!item.meta || !item.meta?.auth || (userExist(item) && roleExist(item))
|
||||||
|
"
|
||||||
|
class="navigation-wrapper"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="item.header"
|
||||||
|
class="text-left font-normal text-xs mx-6 mt-5 mb-2"
|
||||||
|
>
|
||||||
|
<span class="uppercase text-gray-400">
|
||||||
|
{{ item.header ? item.header : "" }}
|
||||||
|
</span>
|
||||||
|
<p class="text-gray-400">
|
||||||
|
{{ item.description ? item.description : "" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul class="navigation-menu">
|
||||||
|
<li
|
||||||
|
class="navigation-item"
|
||||||
|
v-for="(item2, index2) in item.child"
|
||||||
|
:key="index2"
|
||||||
|
@click.stop="
|
||||||
|
item2.child !== undefined ||
|
||||||
|
(item2.child && item2.child.length !== 0)
|
||||||
|
? openMenu($event)
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
!item2.meta ||
|
||||||
|
!item2.meta?.auth ||
|
||||||
|
(userExist(item2) && roleExist(item2))
|
||||||
|
"
|
||||||
|
class="navigation-item-wrapper"
|
||||||
|
>
|
||||||
|
<NuxtLink
|
||||||
|
v-if="
|
||||||
|
item2.child === undefined ||
|
||||||
|
(item2.child && item2.child.length === 0)
|
||||||
|
"
|
||||||
|
class="flex items-center px-6 py-3 cursor-pointer"
|
||||||
|
@click="navigationPage(item2.path, item2.external)"
|
||||||
|
:class="activeMenu(item2.path)"
|
||||||
|
>
|
||||||
|
<Icon v-if="item2.icon" :name="item2.icon" size="18"></Icon>
|
||||||
|
<Icon v-else name="mdi:circle-slice-8" size="18"></Icon>
|
||||||
|
<span class="mx-3 font-normal">{{ item2.title }}</span>
|
||||||
|
<Icon
|
||||||
|
v-if="item2.child && item2.child.length > 0"
|
||||||
|
class="ml-auto side-menu-arrow"
|
||||||
|
name="material-symbols:chevron-right-rounded"
|
||||||
|
size="18"
|
||||||
|
></Icon>
|
||||||
|
</NuxtLink>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
class="flex items-center px-6 py-3 rounded-lg cursor-pointer"
|
||||||
|
:class="activeMenu(item2.path)"
|
||||||
|
>
|
||||||
|
<Icon v-if="item2.icon" :name="item2.icon" size="18"></Icon>
|
||||||
|
<Icon v-else name="mdi:circle-slice-8" size="18"></Icon>
|
||||||
|
<span class="mx-3 font-normal">{{ item2.title }}</span>
|
||||||
|
<Icon
|
||||||
|
v-if="item2.child && item2.child.length > 0"
|
||||||
|
class="ml-auto side-menu-arrow"
|
||||||
|
name="material-symbols:chevron-right-rounded"
|
||||||
|
size="18"
|
||||||
|
></Icon>
|
||||||
|
</a>
|
||||||
|
<RSChildItem
|
||||||
|
v-if="item2.child"
|
||||||
|
:items="item2.child"
|
||||||
|
@openMenu="openMenu"
|
||||||
|
@activeMenu="activeMenu"
|
||||||
|
></RSChildItem>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
145
components/layouts/sidemenu/ItemChild.vue
Normal file
145
components/layouts/sidemenu/ItemChild.vue
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useLayoutStore } from "~/stores/layout";
|
||||||
|
import { useWindowSize } from "vue-window-size";
|
||||||
|
import RSChildItem from "~/components/layouts/sidemenu/ItemChild.vue";
|
||||||
|
import { useUserStore } from "~/stores/user";
|
||||||
|
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
const mobileWidth = layoutStore.mobileWidth;
|
||||||
|
const { width } = useWindowSize();
|
||||||
|
|
||||||
|
const user = useUserStore();
|
||||||
|
const route = useRoute();
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
indent: {
|
||||||
|
type: Number,
|
||||||
|
default: 0.5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(["openMenu"]);
|
||||||
|
|
||||||
|
const indent = ref(props.indent);
|
||||||
|
|
||||||
|
const menuItem = props.items ? props.items : [];
|
||||||
|
|
||||||
|
const username = user.username;
|
||||||
|
const roles = user.roles;
|
||||||
|
|
||||||
|
// validate userExist on meta.auth.user
|
||||||
|
function userExist(item) {
|
||||||
|
if (item.meta?.auth?.user) {
|
||||||
|
if (item.meta?.auth?.user.includes(username)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate roleExist on meta.auth.role
|
||||||
|
function roleExist(item) {
|
||||||
|
if (item.meta?.auth?.role) {
|
||||||
|
if (item.meta?.auth?.role.some((r) => roles.includes(r))) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle Open/Close menu
|
||||||
|
function openMenu(event) {
|
||||||
|
emit("openMenu", event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active menu
|
||||||
|
function activeMenu(routePath) {
|
||||||
|
return route.path == routePath
|
||||||
|
? `bg-[rgb(var(--color-primary))] font-normal text-white active-menu`
|
||||||
|
: `font-light text-white/90 md:transition-all md:duration-200 hover:md:ml-2`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMenu() {
|
||||||
|
document.querySelector(".v-layout").classList.toggle("menu-hide");
|
||||||
|
document.getElementsByClassName("menu-overlay")[0].classList.toggle("hide");
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigationPage(path, external) {
|
||||||
|
if (width.value <= mobileWidth) toggleMenu();
|
||||||
|
navigateTo(path, {
|
||||||
|
external: external,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const indentStyle = computed(() => {
|
||||||
|
return { "background-color": `rgba(var(--sidebar-menu), ${indent.value})` };
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul
|
||||||
|
class="menu-content hide transition-all duration-300"
|
||||||
|
:style="indentStyle"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(item, index) in menuItem"
|
||||||
|
:key="index"
|
||||||
|
@click.stop="
|
||||||
|
item.child !== undefined || (item.child && item.child.length !== 0)
|
||||||
|
? openMenu($event)
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
!item.meta || !item.meta?.auth || (userExist(item) && roleExist(item))
|
||||||
|
"
|
||||||
|
class="navigation-item-wrapper"
|
||||||
|
>
|
||||||
|
<NuxtLink
|
||||||
|
v-if="
|
||||||
|
item.child === undefined || (item.child && item.child.length === 0)
|
||||||
|
"
|
||||||
|
class="flex items-center px-6 py-3 cursor-pointer"
|
||||||
|
@click="navigationPage(item.path, item.external)"
|
||||||
|
:class="activeMenu(item.path)"
|
||||||
|
>
|
||||||
|
<Icon v-if="item.icon" :name="item.icon" size="18"></Icon>
|
||||||
|
<span class="mx-4 font-normal">{{ item.title }}</span>
|
||||||
|
<Icon
|
||||||
|
v-if="item.child && item.child.length > 0"
|
||||||
|
class="ml-auto side-menu-arrow"
|
||||||
|
name="material-symbols:chevron-right-rounded"
|
||||||
|
size="18"
|
||||||
|
></Icon>
|
||||||
|
</NuxtLink>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
class="flex items-center px-6 py-3 rounded-lg cursor-pointer"
|
||||||
|
:class="activeMenu(item.path)"
|
||||||
|
>
|
||||||
|
<span class="mx-3 font-normal">{{ item.title }}</span>
|
||||||
|
<Icon
|
||||||
|
v-if="item.child && item.child.length > 0"
|
||||||
|
class="ml-auto side-menu-arrow"
|
||||||
|
name="material-symbols:chevron-right-rounded"
|
||||||
|
size="18"
|
||||||
|
></Icon>
|
||||||
|
</a>
|
||||||
|
<RSChildItem
|
||||||
|
v-if="item.child"
|
||||||
|
:items="item.child"
|
||||||
|
:indent="indent + 0.1"
|
||||||
|
@openMenu="openMenu"
|
||||||
|
@activeMenu="activeMenu"
|
||||||
|
></RSChildItem>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
72
components/layouts/sidemenu/index.vue
Normal file
72
components/layouts/sidemenu/index.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<script setup>
|
||||||
|
import Menu from "~/navigation/index.js";
|
||||||
|
import RSItem from "~/components/layouts/sidemenu/Item.vue";
|
||||||
|
|
||||||
|
// Use site settings composable
|
||||||
|
const { siteSettings } = useSiteSettings();
|
||||||
|
|
||||||
|
// Add computed to ensure logo reactivity
|
||||||
|
const logoToShow = computed(() => {
|
||||||
|
// Always try to use the siteLogo from settings first
|
||||||
|
if (siteSettings.value?.siteLogo && siteSettings.value.siteLogo.trim() !== "") {
|
||||||
|
return siteSettings.value.siteLogo;
|
||||||
|
}
|
||||||
|
// Fallback to default logo if siteLogo is not set
|
||||||
|
return "/img/logo/corradAF-logo.svg";
|
||||||
|
});
|
||||||
|
|
||||||
|
const siteNameToShow = computed(() => {
|
||||||
|
return siteSettings.value.siteName || "Jabatan Imigresen Malaysia";
|
||||||
|
});
|
||||||
|
|
||||||
|
// const menuItem = Menu;
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
menuItem: {
|
||||||
|
type: Array,
|
||||||
|
default: () => Menu,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
sidebarToggle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
try {
|
||||||
|
const el = document.querySelector(".active-menu").closest(".menu-content");
|
||||||
|
const elParent = el.parentElement.parentElement;
|
||||||
|
|
||||||
|
if (elParent) {
|
||||||
|
elParent.classList.remove("hide");
|
||||||
|
elParent.querySelector("a").classList.add("nav-open");
|
||||||
|
}
|
||||||
|
if (el) el.classList.remove("hide");
|
||||||
|
} catch (e) {
|
||||||
|
// console.log(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="vertical-menu">
|
||||||
|
<div class="py-2 px-4 bg-[rgb(var(--header))]">
|
||||||
|
<nuxt-link to="/">
|
||||||
|
<div class="flex flex-auto gap-3 justify-center items-center h-[48px]">
|
||||||
|
<div
|
||||||
|
class="app-logo text-center h-20 flex justify-center items-center gap-3 px-4"
|
||||||
|
>
|
||||||
|
<nuxt-link to="/" class="flex items-center justify-center">
|
||||||
|
<img src="@/assets/img/logo/lzs-logo.png" class="h-12" alt="logo" />
|
||||||
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<NuxtScrollbar class="flex flex-col justify-between my-6" style="max-height: 82dvh">
|
||||||
|
<RSItem :items="menuItem"></RSItem>
|
||||||
|
</NuxtScrollbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
41
components/layouts/vertical/index.vue
Normal file
41
components/layouts/vertical/index.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useLayoutStore } from "~/stores/layout";
|
||||||
|
|
||||||
|
import RSHeader from "~/components/layouts/Header.vue";
|
||||||
|
import RSSideMenu from "~~/components/layouts/sidemenu/index.vue";
|
||||||
|
// import RSConfigMenu from "~~/components/layouts/configmenu/index.vue";
|
||||||
|
// import RSFooter from "~/components/layouts/Footer.vue";
|
||||||
|
import { useWindowSize } from "vue-window-size";
|
||||||
|
|
||||||
|
const { width } = useWindowSize();
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
const mobileWidth = layoutStore.mobileWidth;
|
||||||
|
|
||||||
|
// watch for window size changes
|
||||||
|
watch(
|
||||||
|
() => [width.value],
|
||||||
|
([width]) => {
|
||||||
|
if (width <= mobileWidth) {
|
||||||
|
document.querySelector(".v-layout").classList.add("menu-hide");
|
||||||
|
document.getElementsByClassName("menu-overlay")[0].classList.add("hide");
|
||||||
|
} else document.querySelector(".v-layout").classList.remove("menu-hide");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function toggleMenu(event) {
|
||||||
|
document.querySelector(".v-layout").classList.toggle("menu-hide");
|
||||||
|
document.getElementsByClassName("menu-overlay")[0].classList.toggle("hide");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RSHeader @toggleMenu="toggleMenu" />
|
||||||
|
<RSSideMenu />
|
||||||
|
<div class="content-page duration-300">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<!-- <RSConfigMenu /> -->
|
||||||
|
<div @click="toggleMenu" class="menu-overlay"></div>
|
||||||
|
|
||||||
|
<!-- <RSFooter /> -->
|
||||||
|
</template>
|
||||||
284
composables/codemirrorThemes.js
Normal file
284
composables/codemirrorThemes.js
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
export default function () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "3024-day",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/3024-day.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "3024-night",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/3024-night.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "abcdef",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/abcdef.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ambiance-mobile",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/ambiance-mobile.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ambiance",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/ambiance.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ayu-dark",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/ayu-dark.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ayu-mirage",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/ayu-mirage.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "base16-dark",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/base16-dark.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "base16-light",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/base16-light.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bespin",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/bespin.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "blackboard",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/blackboard.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cobalt",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/cobalt.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "colorforth",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/colorforth.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dracula",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/dracula.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duotone-dark",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/duotone-dark.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duotone-light",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/duotone-light.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "eclipse",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/eclipse.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "elegant",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/elegant.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "erlang-dark",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/erlang-dark.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gruvbox-dark",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/gruvbox-dark.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hopscotch",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/hopscotch.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "icecoder",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/icecoder.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "idea",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/idea.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "isotope",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/isotope.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lesser-dark",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/lesser-dark.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "liquibyte",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/liquibyte.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lucario",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/lucario.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "material",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/material.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mbo",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/mbo.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mdn-like",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/mdn-like.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "midnight",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/midnight.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "monokai",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/monokai.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "neat",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/neat.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "neo",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/neo.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "night",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/night.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "oceanic-next",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/oceanic-next.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "panda-syntax",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/panda-syntax.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "paraiso-dark",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/paraiso-dark.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "paraiso-light",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/paraiso-light.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pastel-on-dark",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/pastel-on-dark.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "railscasts",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/railscasts.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rubyblue",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/rubyblue.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "seti",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/seti.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "shadowfox",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/shadowfox.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "solarized",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/solarized.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "the-matrix",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/the-matrix.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tomorrow-night-bright",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/tomorrow-night-bright.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tomorrow-night-eighties",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/tomorrow-night-eighties.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ttcn",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/ttcn.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "twilight",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/twilight.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "vibrant-ink",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/vibrant-ink.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "xq-dark",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/xq-dark.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "xq-light",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/xq-light.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "yeti",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/yeti.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "yonce",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/yonce.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zenburn",
|
||||||
|
image:
|
||||||
|
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/zenburn.png",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
20
composables/languageList.js
Normal file
20
composables/languageList.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export default function () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "English",
|
||||||
|
value: "en",
|
||||||
|
flagCode: "GB",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Malay",
|
||||||
|
value: "ms",
|
||||||
|
flagCode: "MY",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Chinese",
|
||||||
|
value: "cn",
|
||||||
|
flagCode: "CN",
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
55
composables/themeList.js
Normal file
55
composables/themeList.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
export default function () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
theme: "biasa",
|
||||||
|
colors: [
|
||||||
|
{
|
||||||
|
name: "primary",
|
||||||
|
value: "243, 88, 106",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secondary",
|
||||||
|
value: "240, 122, 37",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "accent",
|
||||||
|
value: "243, 244, 246",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
theme: "gelap",
|
||||||
|
colors: [
|
||||||
|
{
|
||||||
|
name: "primary",
|
||||||
|
value: "243, 88, 106",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secondary",
|
||||||
|
value: "240, 122, 37",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "accent",
|
||||||
|
value: "15, 23, 42",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
theme: "LZS",
|
||||||
|
colors: [
|
||||||
|
{
|
||||||
|
name: "primary",
|
||||||
|
value: "0, 90, 173", // #005AAD - Blue
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secondary",
|
||||||
|
value: "141, 199, 61", // #8DC73D - Green
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "accent",
|
||||||
|
value: "255, 242, 0", // #FFF200 - Yellow
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
129
composables/themeList2.js
Normal file
129
composables/themeList2.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
export default function () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
theme: "biru",
|
||||||
|
colors: [
|
||||||
|
{
|
||||||
|
name: "primary",
|
||||||
|
value: "0, 102, 204", // Strong blue
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secondary",
|
||||||
|
value: "51, 153, 255", // Lighter blue
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "accent",
|
||||||
|
value: "255, 204, 0", // Gold
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "background",
|
||||||
|
value: "240, 248, 255", // Alice blue
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
value: "0, 0, 0", // Black
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
theme: "merah",
|
||||||
|
colors: [
|
||||||
|
{
|
||||||
|
name: "primary",
|
||||||
|
value: "204, 0, 0", // Strong red
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secondary",
|
||||||
|
value: "255, 102, 102", // Lighter red
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "accent",
|
||||||
|
value: "255, 255, 153", // Light yellow
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "background",
|
||||||
|
value: "255, 240, 240", // Very light pink
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
value: "0, 0, 0", // Black
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
theme: "ungu",
|
||||||
|
colors: [
|
||||||
|
{
|
||||||
|
name: "primary",
|
||||||
|
value: "75, 0, 130", // Indigo
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secondary",
|
||||||
|
value: "138, 43, 226", // Blue violet
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "accent",
|
||||||
|
value: "255, 215, 0", // Gold
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "background",
|
||||||
|
value: "240, 248, 255", // Alice blue
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
value: "0, 0, 0", // Black
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
theme: "oren",
|
||||||
|
colors: [
|
||||||
|
{
|
||||||
|
name: "primary",
|
||||||
|
value: "255, 103, 0", // Dark orange
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secondary",
|
||||||
|
value: "255, 159, 64", // Lighter orange
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "accent",
|
||||||
|
value: "0, 128, 128", // Teal
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "background",
|
||||||
|
value: "255, 250, 240", // Floral white
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
value: "0, 0, 0", // Black
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
theme: "LZS",
|
||||||
|
colors: [
|
||||||
|
{
|
||||||
|
name: "primary",
|
||||||
|
value: "0, 90, 173", // #005AAD - Blue
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secondary",
|
||||||
|
value: "141, 199, 61", // #8DC73D - Green
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "accent",
|
||||||
|
value: "255, 242, 0", // #FFF200 - Yellow
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "background",
|
||||||
|
value: "245, 250, 255", // Very light blue background
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
value: "0, 0, 0", // Black
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user