From ec8f0272ecf77d9116df12b2393c13bc3a69ce4b25c367c7be0b5e316fcafff1 Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Mon, 9 Feb 2026 10:16:41 -0500 Subject: [PATCH] Migrate frontend to Vuetify/Vuexy + add real WHMCS product data - Migrate all frontend from plain JS/Tailwind to TypeScript/Vuetify 3 (Vuexy design system) - Replace placeholder plans with 25 real products scraped from WHMCS: 9 VPS plans ($4.20-$30/mo), 8 dedicated servers ($44.39-$107.99/mo), 4 web hosting plans ($2.39-$15.99/mo), 4 MySQL hosting plans ($6-$30/mo) - Fix Pricing page: correct field mapping (service_type, price), display feature values instead of keys, proper price formatting - Update all marketing pages (Home, Products, VPS, Dedicated, Web Hosting) with real specs, pricing, and features from production WHMCS - Add 38 Vuexy @core SCSS override files for component styling - Create 4 layouts (Account, Admin, Auth, Marketing) with Vuetify - Add AppTextField/AppSelect/AppTextarea wrapper components - Purple primary theme (#7367F0), dark mode default - 52 tests passing, build clean Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 92 +- TASKS.md | 47 +- .../app/Http/Responses/RegisterResponse.php | 18 + website/app/Providers/AppServiceProvider.php | 3 + website/bootstrap/app.php | 1 + website/config/inertia.php | 65 + website/database/seeders/PlanSeeder.php | 496 ++++++- website/env.d.ts | 15 + website/package-lock.json | 1172 +++++++++++------ website/package.json | 16 +- website/resources/css/app.css | 12 - .../resources/images/ezscale_logo_white.png | Bin 0 -> 67228 bytes website/resources/js/Components/Button.vue | 28 - website/resources/js/Components/Card.vue | 12 - .../resources/js/Components/FlashMessages.vue | 16 - website/resources/js/Components/NavLink.vue | 22 - website/resources/js/Layouts/AdminLayout.vue | 58 - website/resources/js/Layouts/AppLayout.vue | 74 -- website/resources/js/Layouts/AuthLayout.vue | 22 - .../resources/js/Pages/Admin/Dashboard.vue | 34 - .../js/Pages/Auth/ConfirmPassword.vue | 44 - .../js/Pages/Auth/ForgotPassword.vue | 52 - website/resources/js/Pages/Auth/Login.vue | 69 - website/resources/js/Pages/Auth/Register.vue | 85 -- .../resources/js/Pages/Auth/ResetPassword.vue | 73 - .../js/Pages/Auth/TwoFactorChallenge.vue | 81 -- .../resources/js/Pages/Auth/VerifyEmail.vue | 37 - website/resources/js/Pages/Billing/Index.vue | 164 --- .../resources/js/Pages/Billing/Invoices.vue | 81 -- .../js/Pages/Billing/Transactions.vue | 80 -- website/resources/js/Pages/Checkout/Show.vue | 191 --- website/resources/js/Pages/Dashboard.vue | 39 - website/resources/js/Pages/Marketing/Home.vue | 41 - website/resources/js/Pages/Plans/Index.vue | 77 -- website/resources/js/Pages/Plans/Show.vue | 59 - website/resources/js/Pages/Profile/Show.vue | 81 -- .../js/Pages/Profile/TwoFactorSetup.vue | 134 -- .../js/Pages/Subscriptions/Index.vue | 78 -- .../resources/js/Pages/Subscriptions/Show.vue | 163 --- website/resources/js/app.js | 24 - .../resources/styles/@core/base/_mixins.scss | 63 + .../resources/styles/@core/base/_utils.scss | 90 ++ .../styles/@core/base/_variables.scss | 198 +++ .../@core/base/libs/vuetify/_index.scss | 1 + .../@core/base/libs/vuetify/_overrides.scss | 262 ++++ .../@core/base/libs/vuetify/_variables.scss | 62 + .../styles/@core/template/_mixins.scss | 6 + .../styles/@core/template/_variables.scss | 102 ++ .../template/libs/vuetify/_variables.scss | 348 +++++ .../libs/vuetify/components/_alert.scss | 114 ++ .../libs/vuetify/components/_avatar.scss | 27 + .../libs/vuetify/components/_badge.scss | 25 + .../libs/vuetify/components/_button.scss | 280 ++++ .../libs/vuetify/components/_cards.scss | 3 + .../libs/vuetify/components/_checkbox.scss | 65 + .../libs/vuetify/components/_chip.scss | 102 ++ .../libs/vuetify/components/_dialog.scss | 27 + .../vuetify/components/_expansion-panels.scss | 106 ++ .../libs/vuetify/components/_field.scss | 308 +++++ .../libs/vuetify/components/_list.scss | 30 + .../libs/vuetify/components/_menu.scss | 35 + .../libs/vuetify/components/_otp-input.scss | 17 + .../libs/vuetify/components/_pagination.scss | 140 ++ .../libs/vuetify/components/_progress.scss | 13 + .../libs/vuetify/components/_radio.scss | 46 + .../libs/vuetify/components/_rating.scss | 20 + .../libs/vuetify/components/_slider.scss | 27 + .../libs/vuetify/components/_snackbar.scss | 10 + .../libs/vuetify/components/_switch.scss | 58 + .../libs/vuetify/components/_table.scss | 48 + .../libs/vuetify/components/_tabs.scss | 91 ++ .../libs/vuetify/components/_textarea.scss | 9 + .../libs/vuetify/components/_timeline.scss | 99 ++ .../libs/vuetify/components/_tooltip.scss | 6 + .../libs/vuetify/components/index.scss | 25 + .../@core/template/libs/vuetify/index.scss | 3 + .../template/libs/vuetify/overrides.scss | 18 + .../@core/template/pages/page-auth.scss | 63 + website/resources/styles/styles.scss | 35 + .../resources/styles/variables/_template.scss | 1 + .../resources/styles/variables/_vuetify.scss | 2 + .../resources/ts/@layouts/styles/_mixins.scss | 30 + .../ts/@layouts/styles/_placeholders.scss | 9 + .../ts/@layouts/styles/_variables.scss | 29 + .../resources/ts/Components/FlashMessages.vue | 16 + website/resources/ts/Components/StatCard.vue | 26 + .../resources/ts/Components/StatusChip.vue | 14 + .../resources/ts/Components/ThemeSwitcher.vue | 15 + .../app-form-elements/AppSelect.vue | 53 + .../app-form-elements/AppTextField.vue | 52 + .../app-form-elements/AppTextarea.vue | 51 + .../resources/ts/Layouts/AccountLayout.vue | 104 ++ website/resources/ts/Layouts/AdminLayout.vue | 120 ++ website/resources/ts/Layouts/AuthLayout.vue | 100 ++ .../resources/ts/Layouts/MarketingLayout.vue | 325 +++++ .../resources/ts/Pages/Admin/Dashboard.vue | 49 + .../ts/Pages/Auth/ConfirmPassword.vue | 57 + .../ts/Pages/Auth/ForgotPassword.vue | 84 ++ website/resources/ts/Pages/Auth/Login.vue | 118 ++ website/resources/ts/Pages/Auth/Register.vue | 129 ++ .../resources/ts/Pages/Auth/ResetPassword.vue | 94 ++ .../ts/Pages/Auth/TwoFactorChallenge.vue | 93 ++ .../resources/ts/Pages/Auth/VerifyEmail.vue | 51 + website/resources/ts/Pages/Billing/Index.vue | 168 +++ .../resources/ts/Pages/Billing/Invoices.vue | 88 ++ .../ts/Pages/Billing/Transactions.vue | 86 ++ website/resources/ts/Pages/Checkout/Show.vue | 199 +++ website/resources/ts/Pages/Dashboard.vue | 73 + .../resources/ts/Pages/Marketing/About.vue | 108 ++ .../resources/ts/Pages/Marketing/Contact.vue | 153 +++ .../ts/Pages/Marketing/DedicatedServers.vue | 304 +++++ .../ts/Pages/Marketing/GameServers.vue | 97 ++ website/resources/ts/Pages/Marketing/Home.vue | 113 ++ .../resources/ts/Pages/Marketing/Pricing.vue | 381 ++++++ .../resources/ts/Pages/Marketing/Products.vue | 102 ++ .../ts/Pages/Marketing/VpsHosting.vue | 169 +++ .../ts/Pages/Marketing/WebHosting.vue | 253 ++++ website/resources/ts/Pages/Plans/Index.vue | 92 ++ website/resources/ts/Pages/Plans/Show.vue | 68 + website/resources/ts/Pages/Profile/Show.vue | 83 ++ .../ts/Pages/Profile/TwoFactorSetup.vue | 149 +++ .../ts/Pages/Subscriptions/Index.vue | 78 ++ .../resources/ts/Pages/Subscriptions/Show.vue | 173 +++ website/resources/ts/app.ts | 30 + .../{js/bootstrap.js => ts/bootstrap.ts} | 0 website/resources/ts/navigation/account.ts | 14 + website/resources/ts/navigation/admin.ts | 5 + website/resources/ts/navigation/marketing.ts | 28 + .../resources/ts/plugins/vuetify/defaults.ts | 128 ++ website/resources/ts/plugins/vuetify/icons.ts | 53 + website/resources/ts/plugins/vuetify/index.ts | 20 + website/resources/ts/plugins/vuetify/theme.ts | 80 ++ website/resources/ts/types/index.ts | 99 ++ website/resources/ts/utils/resolvers.ts | 36 + website/resources/views/app.blade.php | 6 +- website/routes/marketing.php | 19 + website/routes/web.php | 6 - website/tests/Feature/ExampleTest.php | 8 - website/tsconfig.json | 28 + website/vite.config.js | 27 - website/vite.config.ts | 48 + 141 files changed, 9592 insertions(+), 2440 deletions(-) create mode 100644 website/app/Http/Responses/RegisterResponse.php create mode 100644 website/config/inertia.php create mode 100644 website/env.d.ts delete mode 100644 website/resources/css/app.css create mode 100644 website/resources/images/ezscale_logo_white.png delete mode 100644 website/resources/js/Components/Button.vue delete mode 100644 website/resources/js/Components/Card.vue delete mode 100644 website/resources/js/Components/FlashMessages.vue delete mode 100644 website/resources/js/Components/NavLink.vue delete mode 100644 website/resources/js/Layouts/AdminLayout.vue delete mode 100644 website/resources/js/Layouts/AppLayout.vue delete mode 100644 website/resources/js/Layouts/AuthLayout.vue delete mode 100644 website/resources/js/Pages/Admin/Dashboard.vue delete mode 100644 website/resources/js/Pages/Auth/ConfirmPassword.vue delete mode 100644 website/resources/js/Pages/Auth/ForgotPassword.vue delete mode 100644 website/resources/js/Pages/Auth/Login.vue delete mode 100644 website/resources/js/Pages/Auth/Register.vue delete mode 100644 website/resources/js/Pages/Auth/ResetPassword.vue delete mode 100644 website/resources/js/Pages/Auth/TwoFactorChallenge.vue delete mode 100644 website/resources/js/Pages/Auth/VerifyEmail.vue delete mode 100644 website/resources/js/Pages/Billing/Index.vue delete mode 100644 website/resources/js/Pages/Billing/Invoices.vue delete mode 100644 website/resources/js/Pages/Billing/Transactions.vue delete mode 100644 website/resources/js/Pages/Checkout/Show.vue delete mode 100644 website/resources/js/Pages/Dashboard.vue delete mode 100644 website/resources/js/Pages/Marketing/Home.vue delete mode 100644 website/resources/js/Pages/Plans/Index.vue delete mode 100644 website/resources/js/Pages/Plans/Show.vue delete mode 100644 website/resources/js/Pages/Profile/Show.vue delete mode 100644 website/resources/js/Pages/Profile/TwoFactorSetup.vue delete mode 100644 website/resources/js/Pages/Subscriptions/Index.vue delete mode 100644 website/resources/js/Pages/Subscriptions/Show.vue delete mode 100644 website/resources/js/app.js create mode 100644 website/resources/styles/@core/base/_mixins.scss create mode 100644 website/resources/styles/@core/base/_utils.scss create mode 100644 website/resources/styles/@core/base/_variables.scss create mode 100644 website/resources/styles/@core/base/libs/vuetify/_index.scss create mode 100644 website/resources/styles/@core/base/libs/vuetify/_overrides.scss create mode 100644 website/resources/styles/@core/base/libs/vuetify/_variables.scss create mode 100644 website/resources/styles/@core/template/_mixins.scss create mode 100644 website/resources/styles/@core/template/_variables.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/_variables.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_alert.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_avatar.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_badge.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_button.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_cards.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_checkbox.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_chip.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_dialog.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_expansion-panels.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_field.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_list.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_menu.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_otp-input.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_pagination.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_progress.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_radio.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_rating.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_slider.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_snackbar.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_switch.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_table.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_tabs.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_textarea.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_timeline.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/_tooltip.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/components/index.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/index.scss create mode 100644 website/resources/styles/@core/template/libs/vuetify/overrides.scss create mode 100644 website/resources/styles/@core/template/pages/page-auth.scss create mode 100644 website/resources/styles/styles.scss create mode 100644 website/resources/styles/variables/_template.scss create mode 100644 website/resources/styles/variables/_vuetify.scss create mode 100644 website/resources/ts/@layouts/styles/_mixins.scss create mode 100644 website/resources/ts/@layouts/styles/_placeholders.scss create mode 100644 website/resources/ts/@layouts/styles/_variables.scss create mode 100644 website/resources/ts/Components/FlashMessages.vue create mode 100644 website/resources/ts/Components/StatCard.vue create mode 100644 website/resources/ts/Components/StatusChip.vue create mode 100644 website/resources/ts/Components/ThemeSwitcher.vue create mode 100644 website/resources/ts/Components/app-form-elements/AppSelect.vue create mode 100644 website/resources/ts/Components/app-form-elements/AppTextField.vue create mode 100644 website/resources/ts/Components/app-form-elements/AppTextarea.vue create mode 100644 website/resources/ts/Layouts/AccountLayout.vue create mode 100644 website/resources/ts/Layouts/AdminLayout.vue create mode 100644 website/resources/ts/Layouts/AuthLayout.vue create mode 100644 website/resources/ts/Layouts/MarketingLayout.vue create mode 100644 website/resources/ts/Pages/Admin/Dashboard.vue create mode 100644 website/resources/ts/Pages/Auth/ConfirmPassword.vue create mode 100644 website/resources/ts/Pages/Auth/ForgotPassword.vue create mode 100644 website/resources/ts/Pages/Auth/Login.vue create mode 100644 website/resources/ts/Pages/Auth/Register.vue create mode 100644 website/resources/ts/Pages/Auth/ResetPassword.vue create mode 100644 website/resources/ts/Pages/Auth/TwoFactorChallenge.vue create mode 100644 website/resources/ts/Pages/Auth/VerifyEmail.vue create mode 100644 website/resources/ts/Pages/Billing/Index.vue create mode 100644 website/resources/ts/Pages/Billing/Invoices.vue create mode 100644 website/resources/ts/Pages/Billing/Transactions.vue create mode 100644 website/resources/ts/Pages/Checkout/Show.vue create mode 100644 website/resources/ts/Pages/Dashboard.vue create mode 100644 website/resources/ts/Pages/Marketing/About.vue create mode 100644 website/resources/ts/Pages/Marketing/Contact.vue create mode 100644 website/resources/ts/Pages/Marketing/DedicatedServers.vue create mode 100644 website/resources/ts/Pages/Marketing/GameServers.vue create mode 100644 website/resources/ts/Pages/Marketing/Home.vue create mode 100644 website/resources/ts/Pages/Marketing/Pricing.vue create mode 100644 website/resources/ts/Pages/Marketing/Products.vue create mode 100644 website/resources/ts/Pages/Marketing/VpsHosting.vue create mode 100644 website/resources/ts/Pages/Marketing/WebHosting.vue create mode 100644 website/resources/ts/Pages/Plans/Index.vue create mode 100644 website/resources/ts/Pages/Plans/Show.vue create mode 100644 website/resources/ts/Pages/Profile/Show.vue create mode 100644 website/resources/ts/Pages/Profile/TwoFactorSetup.vue create mode 100644 website/resources/ts/Pages/Subscriptions/Index.vue create mode 100644 website/resources/ts/Pages/Subscriptions/Show.vue create mode 100644 website/resources/ts/app.ts rename website/resources/{js/bootstrap.js => ts/bootstrap.ts} (100%) create mode 100644 website/resources/ts/navigation/account.ts create mode 100644 website/resources/ts/navigation/admin.ts create mode 100644 website/resources/ts/navigation/marketing.ts create mode 100644 website/resources/ts/plugins/vuetify/defaults.ts create mode 100644 website/resources/ts/plugins/vuetify/icons.ts create mode 100644 website/resources/ts/plugins/vuetify/index.ts create mode 100644 website/resources/ts/plugins/vuetify/theme.ts create mode 100644 website/resources/ts/types/index.ts create mode 100644 website/resources/ts/utils/resolvers.ts delete mode 100644 website/tests/Feature/ExampleTest.php create mode 100644 website/tsconfig.json delete mode 100644 website/vite.config.js create mode 100644 website/vite.config.ts diff --git a/CLAUDE.md b/CLAUDE.md index 50f6b20..eedd61c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,6 +14,28 @@ All work MUST be tracked against GitHub issues and `TASKS.md`: GitHub issues: https://github.com/EZSCALE/accounting/issues +## Documentation Updates (MANDATORY) +When a phase is completed or a significant task within a phase is finished: + +1. **Update `TASKS.md`** — Check off completed items, add new items discovered during work +2. **Update GitHub issues** — Add progress comments (`gh issue comment --body "..."`) and close completed issues +3. **Update `CLAUDE.md`** — Reflect any new tech stack changes, directory structure changes, or convention updates +4. **Update memory files** — Record key patterns, gotchas, and environment details learned during the work +5. **Update `PROJECT_DEVELOPMENT.md`** — If architectural decisions changed or new integrations were added + +### Visual Verification (MANDATORY) +After every successful `npm run build`: +1. Take headless Chrome screenshots of affected pages +2. Compare layout and design against the Vuexy demo at: https://demos.pixinvent.com/vuexy-vuejs-laravel-admin-template/demo-6/ +3. Fix any visual discrepancies before considering the task complete +4. Key demo pages for reference: + - Login: `.../demo-6/pages/authentication/login-v2` + - Register: `.../demo-6/pages/authentication/register-v2` + - Forgot Password: `.../demo-6/pages/authentication/forgot-password-v2` + - Dashboard: `.../demo-6/dashboards/analytics` + - Account Settings: `.../demo-6/pages/account-settings/account` + - Pricing: `.../demo-6/pages/pricing` + ## Laravel App Location The Laravel application is in **`website/`**. All artisan, composer, and npm commands run from there. @@ -33,12 +55,23 @@ website/ │ ├── factories/ # 7 factories │ └── seeders/ # Roles, plans, admin user ├── resources/ -│ ├── js/ # Vue 3 + Inertia pages (TO BE MIGRATED TO TypeScript) -│ │ ├── Layouts/ # AppLayout, AuthLayout, AdminLayout -│ │ ├── Pages/ # Auth/, Billing/, Plans/, Subscriptions/, Admin/ -│ │ └── Components/ # Card, Button, NavLink, FlashMessages -│ ├── css/app.css # Tailwind CSS 4 -│ └── views/app.blade.php # Inertia root template +│ ├── ts/ # TypeScript source (migrated from js/) +│ │ ├── app.ts # Entry point with Vuetify + Pinia +│ │ ├── bootstrap.ts +│ │ ├── types/ # TypeScript interfaces +│ │ ├── utils/ # Resolvers, formatters +│ │ ├── navigation/ # account.ts, admin.ts, marketing.ts +│ │ ├── plugins/vuetify/ # theme.ts, defaults.ts, icons.ts, index.ts +│ │ ├── @layouts/ # Layout SCSS stubs for Vuexy compatibility +│ │ ├── Layouts/ # AccountLayout, AdminLayout, AuthLayout, MarketingLayout +│ │ ├── Components/ # FlashMessages, StatCard, StatusChip, ThemeSwitcher, app-form-elements/ +│ │ └── Pages/ # Auth/ (7), Profile/ (2), Plans/ (2), Checkout/ (1), Subscriptions/ (2), Billing/ (3), Admin/ (1), Marketing/ (9), Dashboard +│ ├── styles/ # SCSS with Vuexy @core overrides +│ │ ├── @core/ # Copied from Vuexy: base + template SCSS overrides +│ │ ├── variables/ # _vuetify.scss, _template.scss +│ │ └── styles.scss # Main entry — imports @core SCSS chain +│ ├── images/ +│ └── views/app.blade.php # Inertia root template ├── routes/ # web.php, account.php, admin.php, marketing.php, webhooks.php, api.php ├── tests/ # 53 Pest tests (Phase 1 + Phase 2) ├── composer.json @@ -48,8 +81,8 @@ website/ ## Tech Stack - **Framework:** Laravel 12 (PHP 8.3), Laravel 12 slim structure (no Kernel files) -- **Frontend:** Vue 3 + Inertia.js v2 + TypeScript (REQUIRED) + Tailwind CSS 4 + Vite 7 -- **UI Theme:** Vuexy Vue + Laravel Admin Dashboard (reference at `../vuexy-theme-vue-laravel-full-example-typescript/`) +- **Frontend:** Vue 3 + Inertia.js v2 + TypeScript (REQUIRED) + Vuetify 3 (Vuexy design system) + Vite 7 +- **UI Theme:** Vuexy Vue + Laravel Admin Dashboard — SCSS overrides from @core integrated, AppTextField/AppSelect/AppTextarea wrapper components, purple primary (#7367F0) - **Testing:** Pest 4 + PHPUnit 12 - **Formatting:** Laravel Pint - **Payments:** Laravel Cashier (Stripe) + srmklive/paypal (PayPal) @@ -142,6 +175,43 @@ vuexy-theme-vue-laravel-full-example-typescript/ └── tsconfig.json # Strict TypeScript config ``` +## Agent Usage (MANDATORY) +Always maximize use of subagents (Task tool) to reduce context usage in the main conversation. This keeps the main terminal fast and avoids running out of context on large tasks. + +### Rules +- **Parallel agents**: When multiple independent tasks exist (e.g., reading several files, researching different topics, building separate components), launch them as parallel agents in a single message rather than doing them sequentially in the main context. +- **Delegate research**: Use `Explore` agents for codebase exploration, file searches, and understanding existing patterns. Do not manually grep/read dozens of files in the main context. +- **Delegate implementation**: Use `general-purpose` agents for self-contained implementation tasks (e.g., "create this component", "write this migration", "update these 5 files with this pattern"). +- **Delegate reviews**: Use `feature-dev:code-reviewer` agents to review code after writing it. +- **Delegate architecture**: Use `feature-dev:code-architect` or `Plan` agents for designing features before implementing. +- **Keep main context for orchestration**: The main conversation should coordinate agents, communicate with the user, and handle simple/quick edits. Heavy lifting goes to agents. +- **Background agents**: Use `run_in_background: true` for long-running tasks that don't block other work. Check results later with the Read tool on the output file. +- **Batch similar work**: If updating 10+ files with the same pattern, send them to an agent rather than editing each one in the main context. + +### Headless Chrome +Chrome is available for visual testing and screenshot comparison. Use it to verify UI matches design references. +```bash +google-chrome --headless=new --disable-gpu --no-sandbox --screenshot=/tmp/screenshot.png --window-size=1920,1080 --virtual-time-budget=15000 "URL" +``` +- **Must use `--headless=new`** (not just `--headless`) for modern headless mode +- **Must use `--virtual-time-budget=15000`** (or higher) to wait for SPA JavaScript to render before capturing +- **Must use `--no-sandbox`** in this environment +- **Vuexy demo base URL**: https://demos.pixinvent.com/vuexy-vuejs-laravel-admin-template/demo-6/ +- dbus errors in output are harmless — ignore them +- Use for comparing our pages against the Vuexy demo visually +- After every successful `npm run build`, screenshot key pages and compare layout/design against the Vuexy demo to ensure visual parity +- Use for verifying layout, spacing, and component rendering +- Can be delegated to agents for parallel screenshot capture +- Read the resulting PNG with the Read tool to view it + +### Examples of When to Use Agents +- Exploring how a feature works across multiple files → `Explore` agent +- Creating multiple Vue pages/components → parallel `general-purpose` agents +- Writing tests for new code → `general-purpose` agent +- Researching Vuexy theme patterns → `Explore` agent +- Building a new API endpoint (controller + request + test) → `general-purpose` agent +- Reviewing changes before committing → `feature-dev:code-reviewer` agent + ## Code Conventions ### PHP @@ -165,10 +235,12 @@ vuexy-theme-vue-laravel-full-example-typescript/ ### Frontend (Vue/TypeScript) - All components use ` - - diff --git a/website/resources/js/Components/Card.vue b/website/resources/js/Components/Card.vue deleted file mode 100644 index f89188f..0000000 --- a/website/resources/js/Components/Card.vue +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/website/resources/js/Components/FlashMessages.vue b/website/resources/js/Components/FlashMessages.vue deleted file mode 100644 index 6d975b4..0000000 --- a/website/resources/js/Components/FlashMessages.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/website/resources/js/Components/NavLink.vue b/website/resources/js/Components/NavLink.vue deleted file mode 100644 index 666d191..0000000 --- a/website/resources/js/Components/NavLink.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/website/resources/js/Layouts/AdminLayout.vue b/website/resources/js/Layouts/AdminLayout.vue deleted file mode 100644 index 147641c..0000000 --- a/website/resources/js/Layouts/AdminLayout.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - diff --git a/website/resources/js/Layouts/AppLayout.vue b/website/resources/js/Layouts/AppLayout.vue deleted file mode 100644 index 8164a35..0000000 --- a/website/resources/js/Layouts/AppLayout.vue +++ /dev/null @@ -1,74 +0,0 @@ - - - diff --git a/website/resources/js/Layouts/AuthLayout.vue b/website/resources/js/Layouts/AuthLayout.vue deleted file mode 100644 index cbf9b4f..0000000 --- a/website/resources/js/Layouts/AuthLayout.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Admin/Dashboard.vue b/website/resources/js/Pages/Admin/Dashboard.vue deleted file mode 100644 index 389ae68..0000000 --- a/website/resources/js/Pages/Admin/Dashboard.vue +++ /dev/null @@ -1,34 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Auth/ConfirmPassword.vue b/website/resources/js/Pages/Auth/ConfirmPassword.vue deleted file mode 100644 index f0d95ef..0000000 --- a/website/resources/js/Pages/Auth/ConfirmPassword.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Auth/ForgotPassword.vue b/website/resources/js/Pages/Auth/ForgotPassword.vue deleted file mode 100644 index d1c3e58..0000000 --- a/website/resources/js/Pages/Auth/ForgotPassword.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Auth/Login.vue b/website/resources/js/Pages/Auth/Login.vue deleted file mode 100644 index 5447447..0000000 --- a/website/resources/js/Pages/Auth/Login.vue +++ /dev/null @@ -1,69 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Auth/Register.vue b/website/resources/js/Pages/Auth/Register.vue deleted file mode 100644 index beb5ee0..0000000 --- a/website/resources/js/Pages/Auth/Register.vue +++ /dev/null @@ -1,85 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Auth/ResetPassword.vue b/website/resources/js/Pages/Auth/ResetPassword.vue deleted file mode 100644 index 35ad3cf..0000000 --- a/website/resources/js/Pages/Auth/ResetPassword.vue +++ /dev/null @@ -1,73 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Auth/TwoFactorChallenge.vue b/website/resources/js/Pages/Auth/TwoFactorChallenge.vue deleted file mode 100644 index 95471d5..0000000 --- a/website/resources/js/Pages/Auth/TwoFactorChallenge.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Auth/VerifyEmail.vue b/website/resources/js/Pages/Auth/VerifyEmail.vue deleted file mode 100644 index e98f823..0000000 --- a/website/resources/js/Pages/Auth/VerifyEmail.vue +++ /dev/null @@ -1,37 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Billing/Index.vue b/website/resources/js/Pages/Billing/Index.vue deleted file mode 100644 index f4092ba..0000000 --- a/website/resources/js/Pages/Billing/Index.vue +++ /dev/null @@ -1,164 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Billing/Invoices.vue b/website/resources/js/Pages/Billing/Invoices.vue deleted file mode 100644 index 71c08df..0000000 --- a/website/resources/js/Pages/Billing/Invoices.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Billing/Transactions.vue b/website/resources/js/Pages/Billing/Transactions.vue deleted file mode 100644 index a6b28ec..0000000 --- a/website/resources/js/Pages/Billing/Transactions.vue +++ /dev/null @@ -1,80 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Checkout/Show.vue b/website/resources/js/Pages/Checkout/Show.vue deleted file mode 100644 index 81bdd39..0000000 --- a/website/resources/js/Pages/Checkout/Show.vue +++ /dev/null @@ -1,191 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Dashboard.vue b/website/resources/js/Pages/Dashboard.vue deleted file mode 100644 index 4aa8c32..0000000 --- a/website/resources/js/Pages/Dashboard.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Marketing/Home.vue b/website/resources/js/Pages/Marketing/Home.vue deleted file mode 100644 index 730573c..0000000 --- a/website/resources/js/Pages/Marketing/Home.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Plans/Index.vue b/website/resources/js/Pages/Plans/Index.vue deleted file mode 100644 index 3da1d42..0000000 --- a/website/resources/js/Pages/Plans/Index.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Plans/Show.vue b/website/resources/js/Pages/Plans/Show.vue deleted file mode 100644 index 2c74d5f..0000000 --- a/website/resources/js/Pages/Plans/Show.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Profile/Show.vue b/website/resources/js/Pages/Profile/Show.vue deleted file mode 100644 index 8525cbf..0000000 --- a/website/resources/js/Pages/Profile/Show.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Profile/TwoFactorSetup.vue b/website/resources/js/Pages/Profile/TwoFactorSetup.vue deleted file mode 100644 index d9f8057..0000000 --- a/website/resources/js/Pages/Profile/TwoFactorSetup.vue +++ /dev/null @@ -1,134 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Subscriptions/Index.vue b/website/resources/js/Pages/Subscriptions/Index.vue deleted file mode 100644 index 0625838..0000000 --- a/website/resources/js/Pages/Subscriptions/Index.vue +++ /dev/null @@ -1,78 +0,0 @@ - - - diff --git a/website/resources/js/Pages/Subscriptions/Show.vue b/website/resources/js/Pages/Subscriptions/Show.vue deleted file mode 100644 index e0a35cc..0000000 --- a/website/resources/js/Pages/Subscriptions/Show.vue +++ /dev/null @@ -1,163 +0,0 @@ - - - diff --git a/website/resources/js/app.js b/website/resources/js/app.js deleted file mode 100644 index b2bd6d5..0000000 --- a/website/resources/js/app.js +++ /dev/null @@ -1,24 +0,0 @@ -import './bootstrap'; -import '../css/app.css'; - -import { createApp, h } from 'vue'; -import { createInertiaApp } from '@inertiajs/vue3'; -import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; - -const appName = import.meta.env.VITE_APP_NAME || 'EZSCALE'; - -createInertiaApp({ - title: (title) => title ? `${title} - ${appName}` : appName, - resolve: (name) => resolvePageComponent( - `./Pages/${name}.vue`, - import.meta.glob('./Pages/**/*.vue'), - ), - setup({ el, App, props, plugin }) { - return createApp({ render: () => h(App, props) }) - .use(plugin) - .mount(el); - }, - progress: { - color: '#4B5563', - }, -}); diff --git a/website/resources/styles/@core/base/_mixins.scss b/website/resources/styles/@core/base/_mixins.scss new file mode 100644 index 0000000..0fa189c --- /dev/null +++ b/website/resources/styles/@core/base/_mixins.scss @@ -0,0 +1,63 @@ +@use "sass:map"; +@use "@styles/variables/vuetify.scss"; + +@mixin elevation($z, $important: false) { + box-shadow: map.get(vuetify.$shadow-key-umbra, $z), map.get(vuetify.$shadow-key-penumbra, $z), map.get(vuetify.$shadow-key-ambient, $z) if($important, !important, null); +} + +// #region before-pseudo +// ℹ️ This mixin is inspired from vuetify for adding hover styles via before pseudo element +@mixin before-pseudo() { + position: relative; + + &::before { + position: absolute; + border-radius: inherit; + background: currentcolor; + block-size: 100%; + content: ""; + inline-size: 100%; + inset: 0; + opacity: 0; + pointer-events: none; + } +} + +// #endregion before-pseudo + +@mixin bordered-skin($component, $border-property: "border", $important: false) { + #{$component} { + box-shadow: none !important; + // stylelint-disable-next-line annotation-no-unknown + #{$border-property}: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) if($important, !important, null); + } +} + +// #region selected-states +// ℹ️ Inspired from vuetify's active-states mixin +// focus => 0.12 & selected => 0.08 +@mixin selected-states($selector) { + #{$selector} { + opacity: calc(var(--v-selected-opacity) * var(--v-theme-overlay-multiplier)); + } + + &:hover + #{$selector} { + opacity: calc(var(--v-selected-opacity) + var(--v-hover-opacity) * var(--v-theme-overlay-multiplier)); + } + + &:focus-visible + #{$selector} { + opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier)); + } + + @supports not selector(:focus-visible) { + &:focus { + #{$selector} { + opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier)); + } + } + } +} + +// #endregion selected-states diff --git a/website/resources/styles/@core/base/_utils.scss b/website/resources/styles/@core/base/_utils.scss new file mode 100644 index 0000000..ce84357 --- /dev/null +++ b/website/resources/styles/@core/base/_utils.scss @@ -0,0 +1,90 @@ +@use "sass:map"; +@use "sass:list"; +@use "@configured-variables" as variables; + +// Thanks: https://css-tricks.com/snippets/sass/deep-getset-maps/ +@function map-deep-get($map, $keys...) { + @each $key in $keys { + $map: map.get($map, $key); + } + + @return $map; +} + +@function map-deep-set($map, $keys, $value) { + $maps: ($map,); + $result: null; + + // If the last key is a map already + // Warn the user we will be overriding it with $value + @if type-of(nth($keys, -1)) == "map" { + @warn "The last key you specified is a map; it will be overrided with `#{$value}`."; + } + + // If $keys is a single key + // Just merge and return + @if length($keys) == 1 { + @return map-merge($map, ($keys: $value)); + } + + // Loop from the first to the second to last key from $keys + // Store the associated map to this key in the $maps list + // If the key doesn't exist, throw an error + @for $i from 1 through length($keys) - 1 { + $current-key: list.nth($keys, $i); + $current-map: list.nth($maps, -1); + $current-get: map.get($current-map, $current-key); + + @if not $current-get { + @error "Key `#{$key}` doesn't exist at current level in map."; + } + + $maps: list.append($maps, $current-get); + } + + // Loop from the last map to the first one + // Merge it with the previous one + @for $i from length($maps) through 1 { + $current-map: list.nth($maps, $i); + $current-key: list.nth($keys, $i); + $current-val: if($i == list.length($maps), $value, $result); + $result: map.map-merge($current-map, ($current-key: $current-val)); + } + + // Return result + @return $result; +} + +// font size utility classes +@each $name, $size in variables.$font-sizes { + .text-#{$name} { + font-size: $size; + line-height: map.get(variables.$font-line-height, $name); + } +} + +// truncate utility class +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +// gap utility class +@each $name, $size in variables.$gap { + .gap-#{$name} { + gap: $size; + } + + .gap-x-#{$name} { + column-gap: $size; + } + + .gap-y-#{$name} { + row-gap: $size; + } +} + +.list-none { + list-style-type: none; +} diff --git a/website/resources/styles/@core/base/_variables.scss b/website/resources/styles/@core/base/_variables.scss new file mode 100644 index 0000000..71b3529 --- /dev/null +++ b/website/resources/styles/@core/base/_variables.scss @@ -0,0 +1,198 @@ +@use "vuetify/lib/styles/tools/functions" as *; + +/* + TODO: Add docs on when to use placeholder vs when to use SASS variable + + Placeholder + - When we want to keep customization to our self between templates use it + + Variables + - When we want to allow customization from both user and our side + - You can also use variable for consistency (e.g. mx 1 rem should be applied to both vertical nav items and vertical nav header) +*/ + +@forward "@layouts/styles/variables" with ( + // Adjust z-index so vertical nav & overlay stays on top of v-layout in v-main. E.g. Email app + $layout-vertical-nav-z-index: 1003, + $layout-overlay-z-index: 1002, +); +@use "@layouts/styles/variables" as *; + +// 👉 Default layout + +$navbar-high-emphasis-text: true !default; + +// @forward "@layouts/styles/variables" with ( +// $layout-vertical-nav-width: 350px !default, +// ); + +$theme-colors-name: ( + "primary", + "secondary", + "error", + "info", + "success", + "warning" +) !default; + +// 👉 Default layout with vertical nav + +$default-layout-with-vertical-nav-navbar-footer-roundness: 10px !default; + +// 👉 Vertical nav +$vertical-nav-background-color-rgb: var(--v-theme-background) !default; +$vertical-nav-background-color: rgb(#{$vertical-nav-background-color-rgb}) !default; + +// ℹ️ This is used to keep consistency between nav items and nav header left & right margin +// This is used by nav items & nav header +$vertical-nav-horizontal-spacing: 1rem !default; +$vertical-nav-horizontal-padding: 0.75rem !default; + +// Vertical nav header height. Mostly we will align it with navbar height; +$vertical-nav-header-height: $layout-vertical-nav-navbar-height !default; +$vertical-nav-navbar-elevation: 3 !default; +$vertical-nav-navbar-style: "elevated" !default; // options: elevated, floating +$vertical-nav-floating-navbar-top: 1rem !default; + +// Vertical nav header padding +$vertical-nav-header-padding: 1rem $vertical-nav-horizontal-padding !default; +$vertical-nav-header-inline-spacing: $vertical-nav-horizontal-spacing !default; + +// Move logo when vertical nav is mini (collapsed but not hovered) +$vertical-nav-header-logo-translate-x-when-vertical-nav-mini: -4px !default; + +// Space between logo and title +$vertical-nav-header-logo-title-spacing: 0.9rem !default; + +// Section title margin top (when its not first child) +$vertical-nav-section-title-mt: 1.5rem !default; + +// Section title margin bottom +$vertical-nav-section-title-mb: 0.5rem !default; + +// Vertical nav icons +$vertical-nav-items-icon-size: 1.5rem !default; +$vertical-nav-items-nested-icon-size: 0.9rem !default; +$vertical-nav-items-icon-margin-inline-end: 0.5rem !default; + +// Transition duration for nav group arrow +$vertical-nav-nav-group-arrow-transition-duration: 0.15s !default; + +// Timing function for nav group arrow +$vertical-nav-nav-group-arrow-transition-timing-function: ease-in-out !default; + +// 👉 Horizontal nav + +/* + ❗ Heads up + ================== + Here we assume we will always use shorthand property which will apply same padding on four side + This is because this have been used as value of top property by `.popper-content` +*/ +$horizontal-nav-padding: 0.6875rem !default; + +// Gap between top level horizontal nav items +$horizontal-nav-top-level-items-gap: 4px !default; + +// Horizontal nav icons +$horizontal-nav-items-icon-size: 1.5rem !default; +$horizontal-nav-third-level-icon-size: 0.9rem !default; +$horizontal-nav-items-icon-margin-inline-end: 0.625rem !default; +$horizontal-nav-group-arrow-icon-size: 1.375rem !default; + +// ℹ️ We used SCSS variable because we want to allow users to update max height of popper content +// 120px is combined height of navbar & horizontal nav +$horizontal-nav-popper-content-max-height: calc(100dvh - 120px - 4rem) !default; + +// ℹ️ This variable is used for horizontal nav popper content's `margin-top` and "The bridge"'s height. We need to sync both values. +$horizontal-nav-popper-content-top: calc($horizontal-nav-padding + 0.375rem) !default; + +// 👉 Plugins + +$plugin-ps-thumb-y-dark: rgba(var(--v-theme-surface-variant), 0.35) !default; + +// 👉 Vuetify + +// Used in src/@core-scss/base/libs/vuetify/_overrides.scss +$vuetify-reduce-default-compact-button-icon-size: true !default; + +// 👉 Custom variables +// for utility classes +$font-sizes: () !default; +$font-sizes: map-deep-merge( + ( + "xs": 0.75rem, + "sm": 0.875rem, + "base": 1rem, + "lg": 1.125rem, + "xl": 1.25rem, + "2xl": 1.5rem, + "3xl": 1.875rem, + "4xl": 2.25rem, + "5xl": 3rem, + "6xl": 3.75rem, + "7xl": 4.5rem, + "8xl": 6rem, + "9xl": 8rem + ), + $font-sizes +); + +// line height +$font-line-height: () !default; +$font-line-height: map-deep-merge( + ( + "xs": 1rem, + "sm": 1.25rem, + "base": 1.5rem, + "lg": 1.75rem, + "xl": 1.75rem, + "2xl": 2rem, + "3xl": 2.25rem, + "4xl": 2.5rem, + "5xl": 1, + "6xl": 1, + "7xl": 1, + "8xl": 1, + "9xl": 1 + ), + $font-line-height +); + +// gap utility class +$gap: () !default; +$gap: map-deep-merge( + ( + "0": 0, + "1": 0.25rem, + "2": 0.5rem, + "3": 0.75rem, + "4": 1rem, + "5": 1.25rem, + "6":1.5rem, + "7": 1.75rem, + "8": 2rem, + "9": 2.25rem, + "10": 2.5rem, + "11": 2.75rem, + "12": 3rem, + "14": 3.5rem, + "16": 4rem, + "20": 5rem, + "24": 6rem, + "28": 7rem, + "32": 8rem, + "36": 9rem, + "40": 10rem, + "44": 11rem, + "48": 12rem, + "52": 13rem, + "56": 14rem, + "60": 15rem, + "64": 16rem, + "72": 18rem, + "80": 20rem, + "96": 24rem + ), + $gap +); diff --git a/website/resources/styles/@core/base/libs/vuetify/_index.scss b/website/resources/styles/@core/base/libs/vuetify/_index.scss new file mode 100644 index 0000000..f44f80d --- /dev/null +++ b/website/resources/styles/@core/base/libs/vuetify/_index.scss @@ -0,0 +1 @@ +@use "overrides"; diff --git a/website/resources/styles/@core/base/libs/vuetify/_overrides.scss b/website/resources/styles/@core/base/libs/vuetify/_overrides.scss new file mode 100644 index 0000000..c34bf3c --- /dev/null +++ b/website/resources/styles/@core/base/libs/vuetify/_overrides.scss @@ -0,0 +1,262 @@ +@use "@core-scss/base/utils"; +@use "@configured-variables" as variables; + +// 👉 Application +// ℹ️ We need accurate vh in mobile devices as well +.v-application__wrap { + /* stylelint-disable-next-line liberty/use-logical-spec */ + min-height: 100dvh; +} + +// 👉 Typography +h1, +h2, +h3, +h4, +h5, +h6, +.text-h1, +.text-h2, +.text-h3, +.text-h4, +.text-h5, +.text-h6, +.text-button, +.text-overline, +.v-card-title { + color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity)); +} + +body, +.text-body-1, +.text-body-2, +.text-subtitle-1, +.text-subtitle-2 { + color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity)); +} + +// 👉 Grid +// Remove margin-bottom of v-input_details inside grid (validation error message) +.v-row { + .v-col, + [class^="v-col-*"] { + .v-input__details { + margin-block-end: 0; + } + } +} + +// 👉 Button +// Update tonal variant disabled opacity +.v-btn--disabled { + opacity: 0.65; +} + +@if variables.$vuetify-reduce-default-compact-button-icon-size { + .v-btn--density-compact.v-btn--size-default { + .v-btn__content > svg { + block-size: 22px; + font-size: 22px; + inline-size: 22px; + } + } +} + +// 👉 Card + +// Removes padding-top for immediately placed v-card-text after itself +.v-card-text { + & + & { + padding-block-start: 0 !important; + } +} + +/* + 👉 Checkbox & Radio Ripple + + TODO Checkbox and switch component. Remove it when vuetify resolve the extra spacing: https://github.com/vuetifyjs/vuetify/issues/15519 + We need this because form elements likes checkbox and switches are by default set to height of textfield height which is way big than we want + Tested with checkbox & switches +*/ +.v-checkbox.v-input, +.v-switch.v-input { + --v-input-control-height: auto; + + flex: unset; +} + +.v-radio-group { + .v-selection-control-group { + .v-radio:not(:last-child) { + margin-inline-end: 0.9rem; + } + } +} + +/* + 👉 Tabs + Disable tab transition + + This is for tabs where we don't have card wrapper to tabs and have multiple cards as tab content. + + This class will disable transition and adds `overflow: unset` on `VWindow` to allow spreading shadow +*/ +.disable-tab-transition { + overflow: unset !important; + + .v-window__container { + block-size: auto !important; + } + + .v-window-item:not(.v-window-item--active) { + display: none !important; + } + + .v-window__container .v-window-item { + transform: none !important; + } +} + +// 👉 List +.v-list { + // Set icons opacity to .87 + .v-list-item__prepend > .v-icon, + .v-list-item__append > .v-icon { + opacity: 1; + } +} + +// 👉 Card list + +/* + ℹ️ Custom class + + Remove list spacing inside card + + This is because card title gets padding of 20px and list item have padding of 16px. Moreover, list container have padding-bottom as well. +*/ +.card-list { + --v-card-list-gap: 20px; + + &.v-list { + padding-block: 0; + } + + .v-list-item { + min-block-size: unset; + min-block-size: auto !important; + padding-block: 0 !important; + padding-inline: 0 !important; + + > .v-ripple__container { + opacity: 0; + } + + &:not(:last-child) { + padding-block-end: var(--v-card-list-gap) !important; + } + } + + .v-list-item:hover, + .v-list-item:focus, + .v-list-item:active, + .v-list-item.active { + > .v-list-item__overlay { + opacity: 0 !important; + } + } +} + +// 👉 Divider +.v-divider { + color: rgb(var(--v-border-color)); +} + +.v-divider.v-divider--vertical { + block-size: inherit; +} + +// 👉 DataTable +.v-data-table { + /* stylelint-disable-next-line no-descending-specificity */ + .v-checkbox-btn .v-selection-control__wrapper { + margin-inline-start: 0 !important; + } + + .v-selection-control { + display: flex !important; + } + + .v-pagination { + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); + } +} + +// 👉 v-field +.v-field:hover .v-field__outline { + --v-field-border-opacity: var(--v-medium-emphasis-opacity); +} + +// 👉 VLabel +.v-label { + opacity: 1 !important; + + &:not(.v-field-label--floating) { + color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity)); + } +} + +// 👉 Overlay +.v-overlay__scrim, +.v-navigation-drawer__scrim { + background: rgba(var(--v-overlay-scrim-background), var(--v-overlay-scrim-opacity)) !important; + opacity: 1 !important; +} + +// 👉 VMessages +.v-messages { + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); + opacity: 1 !important; +} + +// 👉 Alert close btn +.v-alert__close { + .v-btn--icon .v-icon { + --v-icon-size-multiplier: 1.5; + } +} + +// 👉 Badge icon alignment +.v-badge__badge { + display: flex; + align-items: center; +} + +// 👉 Btn focus outline style removed +.v-btn:focus-visible::after { + opacity: 0 !important; +} + +// .v-select chip spacing for slot +.v-input:not(.v-select--chips) .v-select__selection { + .v-chip { + margin-block: 2px var(--select-chips-margin-bottom); + } +} + +// 👉 VCard and VList subtitle color +.v-list-item-subtitle { + color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity)); +} + +// 👉 placeholders +.v-field__input { + @at-root { + & input::placeholder, + input#{&}::placeholder, + textarea#{&}::placeholder { + color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)) !important; + opacity: 1 !important; + } + } +} diff --git a/website/resources/styles/@core/base/libs/vuetify/_variables.scss b/website/resources/styles/@core/base/libs/vuetify/_variables.scss new file mode 100644 index 0000000..fef87f0 --- /dev/null +++ b/website/resources/styles/@core/base/libs/vuetify/_variables.scss @@ -0,0 +1,62 @@ +@use "sass:map"; + +/* 👉 Shadow opacities */ +$shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity); +$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity); +$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity); + +/* 👉 Card transition properties */ +$card-transition-property-custom: box-shadow, opacity; + +@forward "vuetify/settings" with ( + // 👉 General settings + $color-pack: false !default, + + // 👉 Shadow opacity + $shadow-key-umbra-opacity: $shadow-key-umbra-opacity-custom !default, + $shadow-key-penumbra-opacity: $shadow-key-penumbra-opacity-custom !default, + $shadow-key-ambient-opacity: $shadow-key-ambient-opacity-custom !default, + + // 👉 Card + $card-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default, + $card-elevation: 6 !default, + $card-title-line-height: 1.6 !default, + $card-actions-min-height: unset !default, + $card-text-padding: 1.25rem !default, + $card-item-padding: 1.25rem !default, + $card-actions-padding: 0 12px 12px !default, + $card-transition-property: $card-transition-property-custom !default, + $card-subtitle-opacity: 1 !default, + + // 👉 Expansion Panel + $expansion-panel-active-title-min-height: 48px !default, + + // 👉 List + $list-item-icon-margin-end: 16px !default, + $list-item-icon-margin-start: 16px !default, + $list-item-subtitle-opacity: 1 !default, + + // 👉 Navigation Drawer + $navigation-drawer-content-overflow-y: hidden !default, + + // 👉 Tooltip + $tooltip-background-color: rgba(59, 55, 68, 0.9) !default, + $tooltip-text-color: rgb(var(--v-theme-on-primary)) !default, + $tooltip-font-size: 0.75rem !default, + + // 👉 VTimeline + $timeline-dot-size: 34px !default, + + // 👉 table + $table-transition-property: height !default, + + // 👉 VOverlay + $overlay-opacity: 1 !default, + + // 👉 VContainer + $container-max-widths: ( + "xl": 1440px, + "xxl": 1440px + ) !default, + +); diff --git a/website/resources/styles/@core/template/_mixins.scss b/website/resources/styles/@core/template/_mixins.scss new file mode 100644 index 0000000..6a97343 --- /dev/null +++ b/website/resources/styles/@core/template/_mixins.scss @@ -0,0 +1,6 @@ +@use "sass:map"; +@use "@configured-variables" as variables; + +@mixin custom-elevation($color, $size) { + box-shadow: (map.get(variables.$shadow-params, $size) rgba($color, map.get(variables.$shadow-opacity, $size))); +} diff --git a/website/resources/styles/@core/template/_variables.scss b/website/resources/styles/@core/template/_variables.scss new file mode 100644 index 0000000..56df826 --- /dev/null +++ b/website/resources/styles/@core/template/_variables.scss @@ -0,0 +1,102 @@ +@forward "@core-scss/base/variables" with ( + $default-layout-with-vertical-nav-navbar-footer-roundness: 6px !default, + + $vertical-nav-navbar-style: "floating" !default, // options: elevated, floating + + // 👉 Vertical nav + $vertical-nav-background-color-rgb: var(--v-theme-surface) !default, + // ℹ️ This is used to keep consistency between nav items and nav header left & right margin + // This is used by nav items & nav header + $vertical-nav-horizontal-spacing: 0.75rem !default, + + // Section title margin top (when its not first child) + $vertical-nav-section-title-mt: 1rem !default, + $vertical-nav-navbar-elevation: 4 !default, + $vertical-nav-horizontal-padding: 0.75rem !default, + $layout-vertical-nav-collapsed-width: 70px !default, + + // Move logo when vertical nav is mini (collapsed but not hovered) + $vertical-nav-header-logo-translate-x-when-vertical-nav-mini: -1px !default, + + // Section title margin bottom + $vertical-nav-section-title-mb: 0.375rem !default, + + // Vertical nav header padding + $vertical-nav-header-padding: 1.25rem 0.5rem !default, + + // Vertical nav icons + $vertical-nav-items-icon-size: 1.375rem !default, + $vertical-nav-items-nested-icon-size: 0.75rem !default, + + // 👉Footer + $layout-vertical-nav-footer-height: 54px !default, + + // Gap between top level horizontal nav items + $horizontal-nav-top-level-items-gap: 6px !default, + + $horizontal-nav-items-icon-margin-inline-end: 0.5rem !default, + + $horizontal-nav-popper-content-top: 0.375rem !default, + + $horizontal-nav-group-arrow-icon-size: 1.25rem !default, + $horizontal-nav-third-level-icon-size: 0.75rem !default, + /* + ❗ Heads up + ================== + Here we assume we will always use shorthand property which will apply same padding on four side + This is because this have been used as value of top property by `.popper-content` + */ + $horizontal-nav-padding: 0.5rem !default, + + // 👉 Navbar + $layout-vertical-nav-navbar-height: 54px !default, + $layout-horizontal-nav-navbar-height: 54px !default, + + // Font sizes + $font-sizes: ( + "xs": 0.6875rem, + "sm": 0.8125rem, + "base": 0.9375rem, + "lg": 1.125rem, + "xl": 1.5rem, + "2xl": 1.75rem, + "3xl": 2rem, + "4xl": 2.375rem, + "5xl": 3rem, + "6xl": 3.5rem, + "7xl": 4rem, + "8xl": 4.5rem, + "9xl": 5.25rem, + ) !default, + + // Line heights + $font-line-height: ( + "xs": 0.9375rem, + "sm": 1.25rem, + "base": 1.375rem, + "lg": 1.75rem, + "xl": 2.375rem, + "2xl": 2.625rem, + "3xl": 2.75rem, + "4xl": 3.25rem, + "5xl": 1, + "6xl": 1, + "7xl": 1, + "8xl": 1, + "9xl": 1 + ) !default, +); + +/* Custom shadow opacity */ +$shadow-opacity: ( + "sm": 0.3, + "md": 0.4, + "lg": 0.5, +) !default; + +/* Custom shadow params */ +$shadow-params: ( + "sm": 0 2px 6px 0, + "md": 0 4px 16px 0, + "lg": 0 6px 20px 0, +) !default; diff --git a/website/resources/styles/@core/template/libs/vuetify/_variables.scss b/website/resources/styles/@core/template/libs/vuetify/_variables.scss new file mode 100644 index 0000000..b9f2891 --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/_variables.scss @@ -0,0 +1,348 @@ +@use "sass:math"; + +$font-family-custom: "Public Sans",sans-serif,-apple-system,blinkmacsystemfont, + "Segoe UI",roboto,"Helvetica Neue",arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; + +/* 👉 Typography custom variables */ +$typography-h5-font-size: 1.125rem; +$typography-body-1-font-size: 0.9375rem; +$typography-body-1-line-height: 1.375rem; + +@forward "../../../base/libs/vuetify/variables" with ( + $body-font-family: $font-family-custom !default, + $border-radius-root: 6px !default, + + // 👉 Rounded + $rounded: ( + "sm": 4px, + "lg": 8px, + "shaped": 30px 0, + ) !default, + + // 👉 Shadows + $shadow-key-umbra: ( + 0: (0 0 0 0 rgba(var(--v-shadow-key-umbra-color), 1)), + 1: (0 2px 4px rgba(var(--v-shadow-key-umbra-color), 0.12)), + 2: (0 1px 6px rgba(var(--v-shadow-key-umbra-color), var(--v-shadow-xs-opacity))), + 3: (0 3px 8px rgba(var(--v-shadow-key-umbra-color), 0.14)), + 4: (0 2px 8px rgba(var(--v-shadow-key-umbra-color), var(--v-shadow-sm-opacity))), + 5: (0 4px 10px rgba(var(--v-shadow-key-umbra-color), 0.15)), + 6: (0 3px 12px rgba(var(--v-shadow-key-umbra-color), var(--v-shadow-md-opacity))), + 7: (0 4px 18px rgba(var(--v-shadow-key-umbra-color), 0.1)), + 8: (0 4px 18px rgba(var(--v-shadow-key-umbra-color), var(--v-shadow-lg-opacity))), + 9: (0 5px 14px rgba(var(--v-shadow-key-umbra-color), 0.18)), + 10: (0 5px 30px rgba(var(--v-shadow-key-umbra-color), var(--v-shadow-xl-opacity))), + 11: (0 5px 16px rgba(var(--v-shadow-key-umbra-color), 0.2)), + 12: (0 6px 17px rgba(var(--v-shadow-key-umbra-color), 0.22)), + 13: (0 6px 18px rgba(var(--v-shadow-key-umbra-color), 0.22)), + 14: (0 6px 19px rgba(var(--v-shadow-key-umbra-color), 0.24)), + 15: (0 7px 20px rgba(var(--v-shadow-key-umbra-color), 0.24)), + 16: (0 7px 21px rgba(var(--v-shadow-key-umbra-color), 0.26)), + 17: (0 7px 22px rgba(var(--v-shadow-key-umbra-color), 0.26)), + 18: (0 8px 23px rgba(var(--v-shadow-key-umbra-color), 0.28)), + 19: (0 8px 24px 6px rgba(var(--v-shadow-key-umbra-color), 0.28)), + 20: (0 9px 25px rgba(var(--v-shadow-key-umbra-color), 0.3)), + 21: (0 9px 26px rgba(var(--v-shadow-key-umbra-color), 0.32)), + 22: (0 9px 27px rgba(var(--v-shadow-key-umbra-color), 0.32)), + 23: (0 10px 28px rgba(var(--v-shadow-key-umbra-color), 0.34)), + 24: (0 10px 30px rgba(var(--v-shadow-key-umbra-color), 0.34)) + ) !default, + + $shadow-key-penumbra: ( + 0: (0 0 transparent), + 1: (0 0 transparent), + 2: (0 0 transparent), + 3: (0 0 transparent), + 4: (0 0 transparent), + 5: (0 0 transparent), + 6: (0 0 transparent), + 7: (0 0 transparent), + 8: (0 0 transparent), + 9: (0 0 transparent), + 10: (0 0 transparent), + 11: (0 0 transparent), + 12: (0 0 transparent), + 13: (0 0 transparent), + 14: (0 0 transparent), + 15: (0 0 transparent), + 16: (0 0 transparent), + 17: (0 0 transparent), + 18: (0 0 transparent), + 19: (0 0 transparent), + 20: (0 0 transparent), + 21: (0 0 transparent), + 22: (0 0 transparent), + 23: (0 0 transparent), + 24: (0 0 transparent), + ) !default, + + $shadow-key-ambient: ( + 0: (0 0 transparent), + 1: (0 0 transparent), + 2: (0 0 transparent), + 3: (0 0 transparent), + 4: (0 0 transparent), + 5: (0 0 transparent), + 6: (0 0 transparent), + 7: (0 0 transparent), + 8: (0 0 transparent), + 9: (0 0 transparent), + 10: (0 0 transparent), + 11: (0 0 transparent), + 12: (0 0 transparent), + 13: (0 0 transparent), + 14: (0 0 transparent), + 15: (0 0 transparent), + 16: (0 0 transparent), + 17: (0 0 transparent), + 18: (0 0 transparent), + 19: (0 0 transparent), + 20: (0 0 transparent), + 21: (0 0 transparent), + 22: (0 0 transparent), + 23: (0 0 transparent), + 24: (0 0 transparent), + ) !default, + + // 👉 Typography + $typography: ( + "h1": ( + "size": 2.875rem, + "weight": 500, + "line-height": 4.25rem, + "letter-spacing": normal + ), + "h2": ( + "size": 2.375rem, + "weight": 500, + "line-height": 3.5rem, + "letter-spacing": normal + ), + "h3": ( + "size": 1.75rem, + "weight": 500, + "line-height": 2.625rem + ), + "h4": ( + "size": 1.5rem, + "weight": 500, + "line-height": 2.375rem, + "letter-spacing": normal + ), + "h5": ( + "size": $typography-h5-font-size, + "weight": 500, + "line-height": 1.75rem + ), + "h6":( + "size": 0.9375rem, + "line-height": 1.375rem, + "letter-spacing": normal + ), + "body-1":( + "size": $typography-body-1-font-size, + "line-height": $typography-body-1-line-height, + "letter-spacing": normal + ), + "body-2": ( + "size": 0.8125rem, + "line-height": 1.25rem, + "letter-spacing": normal + ), + "subtitle-1":( + "size": 0.9375rem, + "weight": 400, + "line-height": 1.375rem + ), + "subtitle-2": ( + "size": 0.8125rem, + "weight": 400, + "line-height": 1.25rem, + "letter-spacing": normal + ), + "button": ( + "size": 0.9375rem, + "weight": 500, + "line-height": 1.125rem, + "letter-spacing": 0.0269rem, + "text-transform": capitalize + ), + "caption":( + "size": 0.8125rem, + "line-height": 1.125rem, + "letter-spacing": 0.025rem + ), + "overline": ( + "size": 0.75rem, + "weight": 400, + "line-height": 0.875rem, + "letter-spacing": 0.05rem, + ), + ) !default, + + // 👉 Alert + $alert-title-font-size: 1.125rem !default, + $alert-title-line-height: 1.5rem !default, + $alert-border-opacity: 0.38 !default, + + // 👉 Badge + $badge-dot-height: 8px !default, + $badge-dot-width: 8px !default, + $badge-min-width: 24px !default, + $badge-height: 1.5rem !default, + $badge-font-size: 0.8125rem !default, + $badge-border-radius: 12px !default, + $badge-border-color: rgb(var(--v-theme-surface)) !default, + $badge-border-transform: scale(1.5) !default, + $badge-dot-border-width: 2px !default, + + // 👉 Chip + $chip-font-size: 13px !default, + $chip-font-weight: 500 !default, + $chip-label-border-radius: 0.375rem !default, + $chip-height: 32px !default, + $chip-close-size: 1.25rem !default, + $chip-elevation: 0 !default, + + // 👉 Button + $button-height: 38px !default, + $button-padding-ratio: 1.9 !default, + $button-line-height: 1.375rem !default, + $button-disabled-opacity: 0.45 !default, + $button-disabled-overlay: 0.2025 !default, + $button-icon-font-size: 0.9375rem !default, + + // 👉 Button Group + $btn-group-border-radius: 8px !default, + + // 👉 Dialog + $dialog-card-header-padding: 24px 24px 0 !default, + $dialog-card-header-text-padding-top: 24px !default, + $dialog-card-text-padding: 24px !default, + $dialog-elevation: 8 !default, + + // 👉 Card + $card-title-font-size: $typography-h5-font-size !default, + $card-text-font-size: $typography-body-1-font-size !default, + $card-subtitle-font-size: 0.9375rem !default, + $card-subtitle-header-padding: 0 !default, + $card-subtitle-line-height: 1.375rem !default, + $card-title-line-height: 1.75rem !default, + $card-text-padding: 24px !default, + $card-text-line-height: 1.375rem !default, + $card-item-padding: 24px !default, + $card-elevation: 6 !default, + + // 👉 Carousel + $carousel-dot-margin: 0 !default, + $carousel-dot-inactive-opacity: 0.4 !default, + + // 👉 Expansion Panel + $expansion-panel-title-padding: 12px 20px 12px 24px !default, + $expansion-panel-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default, + $expansion-panel-active-title-min-height: 46px !default, + $expansion-panel-title-min-height: 46px !default, + $expansion-panel-text-padding: 0 20px 20px 24px !default, + + // 👉 Field + $field-font-size: 0.9375rem !default, + $input-density: ("default": -2, "comfortable": -4.5, "compact": -6.5) !default, + $field-outline-opacity: 0.22 !default, + $field-border-width: 1px !default, + $field-focused-border-width: 2px !default, + $field-control-affixed-padding: 14px !default, + + // 👉 Input + $input-details-padding-above: 4px !default, + $input-details-font-size: 0.8125rem !default, + + // 👉 List + $list-density: ("default": 0, "comfortable": -1.5, "compact": -2.5) !default, + $list-border-radius: 6px !default, + $list-item-padding: 8px 20px !default, + $list-item-icon-margin-end: 10px !default, + $list-item-icon-margin-start : 12px !default, + $list-item-subtitle-line-height: 20px !default, + $list-subheader-font-size: 13px !default, + $list-subheader-line-height: 1.25rem !default, + $list-subheader-padding-end: 20px !default, + $list-subheader-min-height: 40px !default, + $list-item-avatar-margin-start: 12px !default, + $list-item-avatar-margin-end: 12px !default, + $list-disabled-opacity: 0.4, + + // 👉 label + $label-font-size: 0.9375rem !default, + + // 👉 message + $messages-font-size: 13px !default, + + // 👉 menu + $menu-elevation: 8 !default, + + // 👉 navigation drawer + $navigation-drawer-temporary-elevation: 8 !default, + + // 👉 pagination + $pagination-item-margin: 0.1875rem !default, + + // 👉 Progress Linear + $progress-linear-background-opacity: 1 !default, + + // 👉 Radio + $radio-group-label-selection-group-padding-inline: 0 !default, + + // 👉 slider + $slider-thumb-hover-opacity: var(--v-activated-opacity) !default, + $slider-thumb-label-padding: 2px 10px !default, + $slider-thumb-label-font-size: 0.8125rem !default, + $slider-track-active-size: 6px !default, + + // 👉 select + $select-chips-margin-bottom: ("default": 1, "comfortable": 1, "compact": 1) !default, + + // 👉 snackbar + $snackbar-background: rgb(var(--v-tooltip-background)) !default, + $snackbar-color: rgb(var(--v-theme-surface)) !default, + $snackbar-content-padding: 12px 16px !default, + $snackbar-font-size: 0.8125rem !default, + $snackbar-elevation: 2 !default, + $snackbar-wrapper-min-height:44px !default, + $snackbar-btn-padding: 0 9px !default, + $snackbar-action-margin: 16px !default, + + // 👉 switch + $switch-inset-track-width: 1.875rem !default, + $switch-inset-track-height: 1.125rem !default, + $switch-inset-thumb-height: 0.875rem !default, + $switch-inset-thumb-width: 0.875rem !default, + $switch-inset-thumb-off-height: 0.875rem !default, + $switch-inset-thumb-off-width: 0.875rem !default, + $switch-thumb-elevation: 2 !default, + $switch-track-opacity: 1 !default, + $switch-track-background: rgba(var(--v-theme-on-surface), var(--v-focus-opacity)) !default, + $switch-thumb-background: rgb(var(--v-theme-on-primary)), + + // 👉 table + $table-row-height: 50px !default, + $table-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default, + $table-font-size: 0.9375rem !default, + + // 👉 tabs + $tabs-height: 42px !default, + $tab-min-width: 50px !default, + + // 👉 tooltip + $tooltip-background-color: rgb(var(--v-tooltip-background)) !default, + $tooltip-text-color: rgb(var(--v-theme-surface)) !default, + $tooltip-font-size: 0.8125rem !default, + $tooltip-border-radius: 0.25rem !default, + $tooltip-padding: 5px 12px !default, + + // 👉 timeline + $timeline-dot-size: 34px !default, + $timeline-dot-divider-background: rgba(var(--v-border-color),0.08) !default, + $timeline-divider-line-background: rgba(var(--v-border-color), var(--v-border-opacity)) !default, + $timeline-divider-line-thickness: 1.5px !default, + $timeline-item-padding: 16px !default, +); diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_alert.scss b/website/resources/styles/@core/template/libs/vuetify/components/_alert.scss new file mode 100644 index 0000000..67fadb1 --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_alert.scss @@ -0,0 +1,114 @@ +@use "@core-scss/base/mixins"; +@use "@configured-variables" as variables; + +/* 👉 Alert +/ ℹ️ custom icon styling */ + +$alert-prepend-icon-font-size: 1.375rem !important; + +.v-alert { + .v-alert__content { + line-height: 1.375rem; + } + + &:not(.v-alert--prominent) .v-alert__prepend { + block-size: 1.875rem !important; + inline-size: 1.875rem !important; + + .v-icon { + margin: auto; + block-size: 1.375rem !important; + font-size: 1.375rem !important; + inline-size: 1.375rem !important; + } + } + + .v-alert-title { + margin-block-end: 0.25rem; + } + + .v-alert__close { + .v-btn--icon { + .v-icon { + block-size: 1.25rem; + font-size: 1.25rem; + inline-size: 1.25rem; + } + + .v-btn__overlay, + .v-ripple__container { + opacity: 0; + } + } + } + + &:not(.v-alert--prominent) { + /* stylelint-disable-next-line no-duplicate-selectors */ + .v-alert__prepend { + border-radius: 0.375rem; + } + + &.v-alert--variant-flat, + &.v-alert--variant-elevated { + .v-alert__prepend { + background-color: #fff; + + @include mixins.elevation(2); + } + } + + &.v-alert--variant-tonal { + .v-alert__prepend { + z-index: 1; + background-color: rgb(var(--v-theme-surface)); + } + } + } +} + +.v-alert.v-alert--density-compact { + border-radius: 0.25rem; +} + +.v-alert.v-alert--density-default { + border-radius: 0.5rem; +} + +@each $color-name in variables.$theme-colors-name { + .v-alert { + &:not(.v-alert--prominent) { + &.bg-#{$color-name}, + &.text-#{$color-name} { + .v-alert__prepend .v-icon { + color: rgb(var(--v-theme-#{$color-name})) !important; + } + } + + &.v-alert--variant-tonal { + &.text-#{$color-name}, + &.bg-#{$color-name} { + .v-alert__underlay { + background: rgb(var(--v-theme-#{$color-name})) !important; + } + + .v-alert__prepend { + background-color: rgb(var(--v-theme-#{$color-name})); + + .v-icon { + color: #fff !important; + } + } + } + } + + &.v-alert--variant-outlined { + &.text-#{$color-name}, + &.bg-#{$color-name} { + .v-alert__prepend { + background-color: rgba(var(--v-theme-#{$color-name}), 0.16); + } + } + } + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_avatar.scss b/website/resources/styles/@core/template/libs/vuetify/components/_avatar.scss new file mode 100644 index 0000000..204679c --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_avatar.scss @@ -0,0 +1,27 @@ +@use "@core-scss/base/mixins"; + +// 👉 Avatar +body { + .v-avatar { + .v-icon { + block-size: 1.5rem; + inline-size: 1.5rem; + } + + &.v-avatar--variant-tonal:not([class*="text-"]) { + .v-avatar__underlay { + --v-activated-opacity: 0.08; + } + } + } + + .v-avatar-group { + > * { + &:hover { + transform: translateY(-5px) scale(1); + + @include mixins.elevation(6); + } + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_badge.scss b/website/resources/styles/@core/template/libs/vuetify/components/_badge.scss new file mode 100644 index 0000000..0cf46aa --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_badge.scss @@ -0,0 +1,25 @@ +@use "@configured-variables" as variables; + +// 👉 Badge +.v-badge { + .v-badge__badge .v-icon { + font-size: 0.9375rem; + } + + &.v-badge--bordered:not(.v-badge--dot) { + .v-badge__badge { + &::after { + transform: scale(1.05); + } + } + } + + &.v-badge--tonal { + @each $color-name in variables.$theme-colors-name { + .v-badge__badge.bg-#{$color-name} { + background-color: rgba(var(--v-theme-#{$color-name}), 0.16) !important; + color: rgb(var(--v-theme-#{$color-name})) !important; + } + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_button.scss b/website/resources/styles/@core/template/libs/vuetify/components/_button.scss new file mode 100644 index 0000000..24fc473 --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_button.scss @@ -0,0 +1,280 @@ +@use "sass:list"; +@use "sass:map"; +@use "@core-scss/template/mixins" as templateMixins; +@use "@configured-variables" as variables; + +/* 👉 Button + Above map but opacity values as key and variant as value */ + +$btn-active-overlay-opacity: ( + 0.08: (outlined, flat, text, plain), + 0.24: (tonal), +); +$btn-hover-overlay-opacity: ( + 0: (elevated), + 0.08: (outlined, flat, text, plain), + 0.24: (tonal), +); +$btn-focus-overlay-opacity: ( + 0.08: (outlined, flat, text, plain), + 0.24: (tonal), +); + +body .v-btn { + // ℹ️ This is necessary because as we have darker overlay on hover for elevated variant, button text doesn't get dimmed + // This style is already applied to `.v-icon` + .v-btn__content { + z-index: 0; + } + + transition: all 0.135s ease; /* Add transition */ + + &:active { + transform: scale(0.98); + } + + // Add transition on hover + &:not(.v-btn--loading) .v-btn__overlay { + transition: opacity 0.15s ease-in-out; + will-change: opacity; + } + + // box-shadow + @each $color-name in variables.$theme-colors-name { + &:not(.v-btn--disabled) { + &.bg-#{$color-name}.v-btn--variant-elevated { + &, + &:hover, + &:active, + &:focus { + @include templateMixins.custom-elevation(var(--v-theme-#{$color-name}), "sm"); + } + } + } + } + + /* + Loop over $btn-active-overlay-opacity map and add active styles for each variant. + Group variants with same opacity value. + */ + @each $opacity, $variants in $btn-active-overlay-opacity { + $overlay-selectors: (); + $underlay-selectors: (); + + // append each variant to selectors list + @each $variant in $variants { + $overlay-selectors: list.append($overlay-selectors, "&.v-btn--variant-#{$variant}:active > .v-btn__overlay,"); + $underlay-selectors: list.append($underlay-selectors, "&.v-btn--variant-#{$variant}:active > .v-btn__underlay,"); + } + + #{$overlay-selectors} { + --v-hover-opacity: #{$opacity}; + + opacity: var(--v-hover-opacity); + } + + #{$underlay-selectors} { + opacity: 0; + } + } + + @each $opacity, $variants in $btn-focus-overlay-opacity { + $selectors: (); + + // append each variant to selectors list + @each $variant in $variants { + $selectors: list.append($selectors, "&.v-btn--variant-#{$variant}:focus > .v-btn__overlay,"); + } + + #{$selectors} { + opacity: $opacity; + } + } + + /* + Loop over $btn-hover-overlay-opacity map and add hover styles for each variant. + Group variants with same opacity value. + */ + @each $opacity, $variants in $btn-hover-overlay-opacity { + $selectors: (); + + // append each variant to selectors list + @each $variant in $variants { + $selectors: list.append($selectors, "&.v-btn--variant-#{$variant}:hover > .v-btn__overlay,"); + } + + #{$selectors} { + --v-hover-opacity: #{$opacity}; + } + } + + // Default (elevated) button + &--variant-elevated, + &--variant-flat { + // We want a darken color on hover + &:not(.v-btn--loading, .v-btn--disabled) { + @each $color-name in variables.$theme-colors-name { + &.bg-#{$color-name} { + &:hover, + &:active, + &:focus { + background-color: rgb(var(--v-theme-#{$color-name}-darken-1)) !important; + } + } + } + } + } + + // Outlined button + &:not(.v-btn--icon, .v-tab).v-btn--variant-text { + &.v-btn--size-default { + padding-inline: 0.75rem; + } + + &.v-btn--size-small { + padding-inline: 0.5625rem; + } + + &.v-btn--size-large { + padding-inline: 1rem; + } + } + + // Button border-radius + &:not(.v-btn--icon).v-btn--size-x-small { + border-radius: 2px; + } + + &:not(.v-btn--icon).v-btn--size-small { + border-radius: 4px; + line-height: 1.125rem; + padding-block: 0; + padding-inline: 0.875rem; + } + + &:not(.v-btn--icon).v-btn--size-default { + .v-btn__content, + .v-btn__append, + .v-btn__prepend { + .v-icon { + --v-icon-size-multiplier: 0.7113; + + block-size: 1rem; + font-size: 1rem; + inline-size: 1rem; + } + + .v-icon--start { + margin-inline: -2px 6px; + } + + .v-icon--end { + margin-inline: 6px -2px; + } + } + } + + &:not(.v-btn--icon).v-btn--size-large { + --v-btn-height: 3rem; + + border-radius: 8px; + line-height: 1.625rem; + padding-block: 0; + padding-inline: 1.625rem; + } + + &:not(.v-btn--icon).v-btn--size-x-large { + border-radius: 10px; + } + + // icon buttons + &.v-btn--icon.v-btn--density-default { + block-size: var(--v-btn-height); + inline-size: var(--v-btn-height); + + &.v-btn--size-default { + .v-icon { + --v-icon-size-multiplier: 0.978 !important; + + block-size: 1.375rem; + font-size: 1.375rem; + inline-size: 1.375rem; + } + } + + &.v-btn--size-small { + --v-btn-height: 2.125rem; + + .v-icon { + block-size: 1.25rem; + font-size: 1.25rem; + inline-size: 1.25rem; + } + } + + &.v-btn--size-large { + --v-btn-height: 2.625rem; + + .v-icon { + block-size: 1.75rem; + font-size: 1.75rem; + inline-size: 1.75rem; + } + } + } + + &-group.v-btn-toggle { + .v-btn { + border-radius: 0.5rem; + block-size: 52px !important; + border-inline-end: none; + inline-size: 52px !important; + + &.v-btn--density-comfortable { + border-radius: 0.375rem; + block-size: 44px !important; + inline-size: 44px !important; + } + + &.v-btn--density-compact { + border-radius: 0.25rem; + block-size: 36px !important; + inline-size: 36px !important; + } + + &.v-btn--icon .v-icon { + block-size: 1.5rem; + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); + font-size: 1.5rem; + inline-size: 1.5rem; + } + + &.v-btn--icon.v-btn--active { + .v-icon { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + } + } + } + + &.v-btn-group { + align-items: center; + padding: 7px; + border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + block-size: 66px; + + .v-btn.v-btn--active { + .v-btn__overlay { + --v-activated-opacity: 0.08; + } + } + + &.v-btn-group--density-compact { + block-size: 50px; + } + + &.v-btn-group--density-comfortable { + block-size: 58px; + } + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_cards.scss b/website/resources/styles/@core/template/libs/vuetify/components/_cards.scss new file mode 100644 index 0000000..fe680cb --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_cards.scss @@ -0,0 +1,3 @@ +.v-card-subtitle { + color: rgba(var(--v-theme-on-background), 0.55); +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_checkbox.scss b/website/resources/styles/@core/template/libs/vuetify/components/_checkbox.scss new file mode 100644 index 0000000..8ed13de --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_checkbox.scss @@ -0,0 +1,65 @@ +@use "sass:list"; +@use "sass:map"; +@use "@styles/variables/vuetify"; +@use "@configured-variables" as variables; + +// 👉 Checkbox +.v-checkbox { + // We adjusted it to vertically align the label + + .v-selection-control--disabled { + --v-disabled-opacity: 0.45; + } + + // Remove extra space below the label + .v-input__details { + min-block-size: unset !important; + padding-block-start: 0 !important; + } +} + +// 👉 checkbox size and box shadow +.v-checkbox-btn { + // 👉 Checkbox icon opacity + .v-selection-control__input > .v-icon { + opacity: 1; + } + + &.v-selection-control--dirty { + @each $color-name in variables.$theme-colors-name { + .v-selection-control__wrapper.text-#{$color-name} { + .v-selection-control__input { + /* ℹ️ Using filter: drop-shadow() instead of box-shadow because box-shadow creates white background for SVG; */ + .v-icon { + filter: drop-shadow(0 2px 6px rgba(var(--v-theme-#{$color-name}), 0.3)); + } + } + } + } + } +} + +// checkbox icon size +.v-checkbox, +.v-checkbox-btn { + &.v-selection-control { + .v-selection-control__input { + svg { + font-size: 1.5rem; + } + } + + .v-label { + color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity)); + line-height: 1.375rem; + } + } + + &:not(.v-selection-control--dirty) { + .v-selection-control__input { + > .custom-checkbox-indeterminate { + color: rgb(var(--v-theme-primary)) !important; + } + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_chip.scss b/website/resources/styles/@core/template/libs/vuetify/components/_chip.scss new file mode 100644 index 0000000..85754cd --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_chip.scss @@ -0,0 +1,102 @@ +// 👉 Chip +.v-chip { + line-height: 1.25rem; + + .v-chip__close { + margin-inline: 4px -6px !important; + + .v-icon { + opacity: 0.7; + } + } + + .v-chip__content { + .v-icon { + block-size: 20px; + font-size: 20px; + inline-size: 20px; + } + } + + &:not(.v-chip--variant-elevated) { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + } + + &.v-chip--variant-elevated { + background-color: rgba(var(--v-theme-on-surface), var(--v-activated-opacity)); + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + } + + &:not([class*="text-"]) { + --v-activated-opacity: 0.08; + } + + // common style for all sizes + &.v-chip--size-default, + &.v-chip--size-small { + .v-icon--start, + .v-chip__filter { + margin-inline-start: 0 !important; + } + + &:not(.v-chip--pill) { + .v-avatar--start { + margin-inline: -6px 4px; + } + + .v-avatar--end { + margin-inline: 4px -6px; + } + } + } + + // small size + &:not(.v-chip--pill).v-chip--size-small { + --v-chip-height: 24px; + + &.v-chip--label { + border-radius: 0.25rem; + } + + font-size: 13px; + + .v-avatar { + --v-avatar-height: 16px; + } + + .v-chip__close { + font-size: 16px; + max-block-size: 16px; + max-inline-size: 16px; + } + } + + // extra small size + &:not(.v-chip--pill).v-chip--size-x-small { + --v-chip-height: 20px; + + &.v-chip--label { + border-radius: 0.25rem; + padding-inline: 0.625rem; + } + + font-size: 13px; + + .v-avatar { + --v-avatar-height: 16px; + } + + .v-chip__close { + font-size: 16px; + max-block-size: 16px; + max-inline-size: 16px; + } + } + + // default size + &:not(.v-chip--pill).v-chip--size-default { + .v-avatar { + --v-avatar-height: 20px; + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_dialog.scss b/website/resources/styles/@core/template/libs/vuetify/components/_dialog.scss new file mode 100644 index 0000000..054b4aa --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_dialog.scss @@ -0,0 +1,27 @@ +@use "@layouts/styles/mixins" as layoutsMixins; + +// 👉 Dialog +body .v-dialog { + // dialog custom close btn + .v-dialog-close-btn { + border-radius: 0.375rem; + background-color: rgb(var(--v-theme-surface)) !important; + block-size: 2rem; + inline-size: 2rem; + inset-block-start: 0; + inset-inline-end: 0; + transform: translate(0.5rem, -0.5rem); + + @include layoutsMixins.rtl { + transform: translate(-0.5rem, -0.5rem); + } + + &:hover { + transform: translate(0.3125rem, -0.3125rem); + + @include layoutsMixins.rtl { + transform: translate(-0.3125rem, -0.3125rem); + } + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_expansion-panels.scss b/website/resources/styles/@core/template/libs/vuetify/components/_expansion-panels.scss new file mode 100644 index 0000000..72c8882 --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_expansion-panels.scss @@ -0,0 +1,106 @@ +@use "@core-scss/base/mixins"; +@use "@layouts/styles/mixins" as layoutsMixins; + +// 👉 Expansion panels +body .v-layout .v-application__wrap .v-expansion-panels { + .v-expansion-panel { + margin-block-start: 0 !important; + + // expansion panel arrow font size + .v-expansion-panel-title { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + font-weight: 500; + + .v-expansion-panel-title__icon { + transition: transform 0.2s ease-in-out; + + .v-icon { + block-size: 1.25rem !important; + font-size: 1.25rem !important; + inline-size: 1.25rem !important; + } + } + } + + .v-expansion-panel-title, + .v-expansion-panel-title--active, + .v-expansion-panel-title:hover, + .v-expansion-panel-title:focus, + .v-expansion-panel-title:focus-visible, + .v-expansion-panel-title--active:focus, + .v-expansion-panel-title--active:hover { + .v-expansion-panel-title__overlay { + opacity: 0 !important; + } + } + + // Set Elevation when panel open + &:not(.v-expansion-panels--variant-accordion) { + &.v-expansion-panel--active { + .v-expansion-panel__shadow { + @include mixins.elevation(6); + } + } + } + } + + // custom style for expansion panels + &.expansion-panels-width-border { + border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + border-radius: 0.375rem; + + .v-expansion-panel-title { + background-color: rgb(var(--v-theme-grey-light)); + border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + margin-block-end: -1px; + } + + .v-expansion-panel-text { + .v-expansion-panel-text__wrapper { + padding: 1.25rem; + } + } + } + + &:not(.no-icon-rotate, .expansion-panels-width-border) { + .v-expansion-panel { + .v-expansion-panel-title__icon { + .v-icon { + @include layoutsMixins.rtl { + transform: scaleX(-1); + } + } + } + + &.v-expansion-panel--active { + .v-expansion-panel-title__icon { + transform: rotate(90deg); + + @include layoutsMixins.rtl { + transform: rotate(-90deg); + } + } + } + } + } + + &:not(.expansion-panels-width-border) { + .v-expansion-panel { + &:not(:last-child) { + margin-block-end: 0.5rem; + } + + &:not(:first-child)::after { + content: none; + } + + // ℹ️ we have to use below style of increase the specificity and override the default style + /* stylelint-disable-next-line no-descending-specificity */ + &:first-child:not(:last-child), + &:not(:first-child, :last-child), + &:not(:first-child) { + border-radius: 0.375rem !important; + } + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_field.scss b/website/resources/styles/@core/template/libs/vuetify/components/_field.scss new file mode 100644 index 0000000..cd6b16b --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_field.scss @@ -0,0 +1,308 @@ +@use "sass:map"; +@use "@configured-variables" as variables; +@use "@core-scss/template/mixins" as templateMixins; + +$v-input-density: ( + comfortable: ( + icon-size: 1rem, + font-size: 0.9375rem, + line-height: 1.5rem, + ), + default: ( + icon-size: 1.125rem, + font-size: 1.0625rem, + line-height: 1.5rem, + ), + compact: ( + icon-size: 0.8125rem, + font-size: 0.8125rem, + line-height: 1.375rem, + ), +); + +// 👉 VInput +.v-input { + // 👉 VField + .v-field { + .v-field__loader { + .v-progress-linear { + .v-progress-linear__background { + background-color: transparent !important; + } + } + } + + &.v-field--variant-solo, + &.v-field--variant-filled { + --v-field-padding-top: 7px !important; + + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); + } + + // Color for text field + .v-field__input { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + } + + // Make field border width 2px when error + &.v-field--error { + .v-field__outline { + --v-field-border-width: 2px; + } + } + + // Label + &.v-field--variant-outlined { + .v-label { + &.v-field-label--floating { + line-height: 0.9375rem; + margin-block: 0; + margin-inline: 6px; + } + } + } + + &:not(.v-field--focused, .v-field--error):hover .v-field__outline { + --v-field-border-opacity: 0.6 !important; + } + + // Shadow on focus + &.v-field--variant-outlined.v-field--focused:not(.v-field--error, .v-field--success) { + .v-field__outline { + @each $color-name in variables.$theme-colors-name { + &.text-#{$color-name} { + @include templateMixins.custom-elevation(var(--v-theme-#{$color-name}), "sm"); + } + } + + &:not([class*="text-"]) { + @include templateMixins.custom-elevation(var(--v-theme-primary), "sm"); + } + } + } + } + + // Give hint messages color based on theme color + @each $color-name in variables.$theme-colors-name { + &:has( .v-field.v-field--focused .v-field__outline.text-#{$color-name}) { + .v-messages { + color: rgb(var(--v-theme-#{$color-name})); + } + } + } + + // Loop through each density setting in the map + @each $density, $settings in $v-input-density { + &.v-input--density-#{$density} { + .v-input__append, + .v-input__prepend, + .v-input__details, + .v-field .v-field__append-inner, + .v-field .v-field__prepend-inner, + .v-field .v-field__clearable { + > .v-icon { + block-size: map.get($settings, icon-size); + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + font-size: map.get($settings, icon-size); + inline-size: map.get($settings, icon-size); + opacity: 1; + } + } + + .v-field { + .v-field__input { + font-size: map.get($settings, font-size); + line-height: map.get($settings, line-height); + } + } + } + } +} + +// 👉 TextField, Select, AutoComplete, ComboBox, Textarea +// ℹ️ We added .v-application to increase the specificity of the selector + +// Styles related to our custom input components +body { + .app-text-field, + .app-select, + .app-autocomplete, + .app-combobox, + .app-textarea, + .app-picker-field { + // making padding 0 for help text + .v-text-field .v-input__details { + padding-inline-start: 0; + } + + // Placeholder + .v-input { + .v-field { + // Placeholder transition + input, + .v-field__input { + &::placeholder { + transition: transform 0.2s ease-out; + } + } + + &.v-field--focused { + input, + .v-field__input { + &::placeholder { + transform: translateX(4px) !important; + + [dir="rtl"] & { + transform: translateX(-4px) !important; + } + } + } + } + } + + // padding for different density + &.v-input--density-default { + .v-field { + .v-field__input { + --v-field-padding-start: 16px; + --v-field-padding-end: 16px; + } + } + } + + &.v-input--density-comfortable { + .v-field { + .v-field__input { + --v-field-padding-start: 14px; + --v-field-padding-end: 14px; + } + } + } + + &.v-input--density-compact { + .v-field { + .v-field__input { + --v-field-padding-start: 12px; + --v-field-padding-end: 12px; + } + } + } + } + + // Disabled state + &:has(.v-input.v-input--disabled) { + .v-label { + color: rgba(var(--v-theme-on-surface), 0.38); + } + + .v-input { + .v-field.v-field--disabled { + background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity)); + opacity: 1; + + .v-field__outline { + --v-field-border-opacity: 0.24; + } + + .v-field__input { + color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)); + } + } + } + } + + // Apply color to label + @each $color-name in variables.$theme-colors-name { + .v-label { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + + &:has(+ .v-input .v-field.v-field--focused .v-field__outline.text-#{$color-name}) { + color: rgb(var(--v-theme-#{$color-name})); + } + + &:has(+ .v-input .v-field.v-field--error) { + color: rgb(var(--v-theme-error)); + } + } + } + } + + @mixin style-selectable-component($component-name) { + .app-#{$component-name} { + .v-#{$component-name}__selection { + line-height: 24px; + margin-block: 0 !important; + } + + // Vertical alignment of placeholder & text + .v-#{$component-name} .v-field .v-field__input > input { + align-self: center; + } + + // Chips + + .v-#{$component-name}.v-#{$component-name}--chips.v-input--dirty { + .v-#{$component-name}__selection { + margin: 0; + } + + .v-field__input { + gap: 3px; + } + + &.v-input--density-compact { + .v-field__input { + padding-inline-start: 0.5rem; + } + } + + &.v-input--density-comfortable { + .v-field__input { + padding-inline-start: 0.75rem; + } + } + + &.v-input--density-default { + .v-field__input { + padding-inline-start: 1rem; + } + } + } + } + } + + @include style-selectable-component("autocomplete"); + @include style-selectable-component("select"); + @include style-selectable-component("combobox"); + + // AutoComplete + @at-root { + .app-autocomplete__content { + .v-list-item--active { + .v-autocomplete__mask { + background: rgba(92, 82, 192, 60%); + } + } + + .v-theme--dark { + .v-list-item:not(.v-list-item--active) { + .v-autocomplete__mask { + background: rgba(59, 64, 92, 60%); + } + } + } + } + } +} + +.app-inner-list { + // Hide checkboxes + .v-selection-control { + display: none; + } +} + +// Hide resizer +::-webkit-resizer { + background: transparent; +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_list.scss b/website/resources/styles/@core/template/libs/vuetify/components/_list.scss new file mode 100644 index 0000000..1b1d080 --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_list.scss @@ -0,0 +1,30 @@ +// 👉 List +.v-list-item { + --v-hover-opacity: 0.06 !important; + + .v-checkbox-btn.v-selection-control--density-compact { + margin-inline-end: 0.5rem; + } + + .v-list-item__overlay { + transition: none; + } + + .v-list-item__prepend { + .v-icon { + font-size: 1.375rem; + } + } + + &.v-list-item--active { + &.v-list-group__header { + color: rgb(var(--v-theme-primary)); + } + + &:not(.v-list-group__header) { + .v-list-item-subtitle { + color: rgb(var(--v-theme-primary)); + } + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_menu.scss b/website/resources/styles/@core/template/libs/vuetify/components/_menu.scss new file mode 100644 index 0000000..9bb5329 --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_menu.scss @@ -0,0 +1,35 @@ +// Style list differently when it's used in a components like select, menu etc +.v-menu { + // Adjust padding of list item inside menu + .v-list-item { + padding-block: 8px !important; + padding-inline: 16px !important; + } +} + +// 👉 Menu +// Menu custom style +.v-menu.v-overlay { + .v-overlay__content { + .v-list { + .v-list-item { + border-radius: 0.375rem !important; + margin-block: 0.125rem; + margin-inline: 0.5rem; + min-block-size: 2.375rem; + + &:first-child { + margin-block-start: 0; + } + + &:last-child { + margin-block-end: 0; + } + } + + .v-list-item--density-default:not(.v-list-item--nav).v-list-item--one-line { + padding-block: 0.5rem; + } + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_otp-input.scss b/website/resources/styles/@core/template/libs/vuetify/components/_otp-input.scss new file mode 100644 index 0000000..ed38ad9 --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_otp-input.scss @@ -0,0 +1,17 @@ +// otp input +.v-otp-input { + justify-content: unset !important; + + .v-otp-input__content { + max-inline-size: 100%; + + .v-field.v-field--focused { + .v-field__outline { + .v-field__outline__start, + .v-field__outline__end { + border-color: rgb(var(--v-theme-primary)) !important; + } + } + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_pagination.scss b/website/resources/styles/@core/template/libs/vuetify/components/_pagination.scss new file mode 100644 index 0000000..df3fb5d --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_pagination.scss @@ -0,0 +1,140 @@ +/* stylelint-disable no-descending-specificity */ +@use "@core-scss/template/mixins" as templateMixins; +@use "@configured-variables" as variables; + +// 👉 Pagination +.v-pagination { + // pagination + .v-pagination__next, + .v-pagination__prev { + .v-btn--icon { + &.v-btn--size-small { + .v-icon { + font-size: 1rem; + } + } + + &.v-btn--size-default { + .v-icon { + font-size: 1.125rem; + } + } + + &.v-btn--size-large { + .v-icon { + font-size: 1.5rem; + } + } + } + } + + // common style for all components + .v-pagination__next, + .v-pagination__prev, + .v-pagination__first, + .v-pagination__last, + .v-pagination__item { + .v-btn { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + font-size: 0.8125rem; + font-weight: 400; + line-height: 1; + + --v-activated-opacity: 0.08; + + &:hover { + .v-btn__overlay { + --v-hover-opacity: 0.16; + } + } + + &.v-btn--disabled { + opacity: 0.45; + } + + &.v-btn--size-large { + font-size: 0.9375rem; + } + } + } + + // Disable scale animation for button + .v-pagination__item { + .v-btn { + transform: scale(1) !important; + + /* We disabled transition because it looks ugly 🤮 */ + transition-duration: 0s; + + &:active { + transform: scale(1); + } + } + } + + .v-pagination__list { + @each $color-name in variables.$theme-colors-name { + &:has(.v-pagination__item.v-pagination__item--is-active .v-btn.text-#{$color-name}) { + .v-pagination__item { + .v-btn { + &:hover { + color: rgb(var(--v-theme-#{$color-name})); + + .v-btn__overlay { + background-color: rgb(var(--v-theme-#{$color-name})); + } + } + } + } + } + } + + .v-pagination__item--is-active { + .v-btn { + &:not([class*="text-"]) { + color: rgb(var(--v-theme-primary)); + + &:not(.v-btn--variant-outlined) { + .v-btn__underlay { + --v-activated-opacity: 0.04; + } + } + + &.v-btn--variant-outlined { + border-color: rgb(var(--v-theme-primary)); + + .v-btn__overlay { + opacity: 0.16; + } + } + } + + // box-shadow + @each $color-name in variables.$theme-colors-name { + &:not(.v-btn--disabled) { + &.text-#{$color-name} { + &, + &:hover, + &:active, + &:focus { + @include templateMixins.custom-elevation(var(--v-theme-#{$color-name}), "sm"); + } + + .v-btn__underlay { + opacity: 1 !important; + } + + .v-btn__content { + color: #fff; + } + + &.v-btn--variant-outlined { + background-color: rgb(var(--v-theme-#{$color-name})); + } + } + } + } + } + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_progress.scss b/website/resources/styles/@core/template/libs/vuetify/components/_progress.scss new file mode 100644 index 0000000..2aa0c8f --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_progress.scss @@ -0,0 +1,13 @@ +// @use "@core-scss/template/mixins" as templateMixins; +@use "@configured-variables" as variables; + +// 👉 Progress +// .v-progress-linear { +// .v-progress-linear__determinate { +// @each $color-name in variables.$theme-colors-name { +// &.bg-#{$color-name} { +// // @include templateMixins.custom-elevation(var(--v-theme-#{$color-name}), "sm"); +// } +// } +// } +// } diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_radio.scss b/website/resources/styles/@core/template/libs/vuetify/components/_radio.scss new file mode 100644 index 0000000..0aac48c --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_radio.scss @@ -0,0 +1,46 @@ +@use "@core-scss/base/mixins"; +@use "@configured-variables" as variables; + +// 👉 Radio +.v-radio, +.v-radio-btn { + // 👉 radio icon opacity + .v-selection-control__input > .v-icon { + opacity: 1; + } + + &.v-selection-control--disabled { + --v-disabled-opacity: 0.45; + } + + &.v-selection-control--dirty { + @each $color-name in variables.$theme-colors-name { + .v-selection-control__wrapper.text-#{$color-name} { + .v-selection-control__input { + /* ℹ️ Using filter: drop-shadow() instead of box-shadow because box-shadow creates white background for SVG; */ + .v-icon { + filter: drop-shadow(0 2px 6px rgba(var(--v-theme-#{$color-name}), 0.3)); + } + } + } + } + } + + &.v-selection-control { + .v-selection-control__input { + svg { + font-size: 1.5rem; + } + } + + .v-label { + color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity)); + } + } +} + +// 👉 Radio, Checkbox + +.v-input.v-radio-group > .v-input__control > .v-label { + margin-inline-start: 0; +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_rating.scss b/website/resources/styles/@core/template/libs/vuetify/components/_rating.scss new file mode 100644 index 0000000..2014dc3 --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_rating.scss @@ -0,0 +1,20 @@ +// 👉 Rating +.v-rating { + .v-rating__wrapper { + .v-btn .v-icon { + --v-icon-size-multiplier: 1; + } + + .v-btn--density-default { + --v-btn-height: 26px; + } + + .v-btn--density-compact { + --v-btn-height: 30px; + } + + .v-btn--density-comfortable { + --v-btn-height: 32px; + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_slider.scss b/website/resources/styles/@core/template/libs/vuetify/components/_slider.scss new file mode 100644 index 0000000..cb93267 --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_slider.scss @@ -0,0 +1,27 @@ +// 👉 Slider +.v-slider { + .v-slider-track__background--opacity { + opacity: 0.16; + } +} + +.v-slider-thumb { + .v-slider-thumb__surface::after { + border-radius: 50%; + background-color: #fff; + block-size: calc(var(--v-slider-thumb-size) - 10px); + inline-size: calc(var(--v-slider-thumb-size) - 10px); + } + + .v-slider-thumb__label { + background-color: rgb(var(--v-tooltip-background)); + color: rgb(var(--v-theme-surface)); + font-weight: 500; + letter-spacing: 0.15px; + line-height: 1.25rem; + + &::before { + content: none; + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_snackbar.scss b/website/resources/styles/@core/template/libs/vuetify/components/_snackbar.scss new file mode 100644 index 0000000..61a2b3b --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_snackbar.scss @@ -0,0 +1,10 @@ +// 👉 snackbar +.v-snackbar { + .v-snackbar__actions { + .v-btn { + font-size: 13px; + line-height: 18px; + text-transform: capitalize; + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_switch.scss b/website/resources/styles/@core/template/libs/vuetify/components/_switch.scss new file mode 100644 index 0000000..057223e --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_switch.scss @@ -0,0 +1,58 @@ +@use "@configured-variables" as variables; +@use "@core-scss/template/mixins" as templateMixins; + +// 👉 switch +.v-switch { + &.v-switch--inset { + .v-selection-control { + .v-switch__track { + transition: all 0.1s; + } + + &.v-selection-control--dirty { + @each $color-name in variables.$theme-colors-name { + .v-switch__track.bg-#{$color-name} { + @include templateMixins.custom-elevation(var(--v-theme-#{$color-name}), "sm"); + } + } + } + + &:not(.v-selection-control--dirty) { + .v-switch__track { + box-shadow: 0 0 4px 0 rgba(0, 0, 0, 16%) inset; + } + } + } + + .v-selection-control__input { + transform: translateX(-6px) !important; + + --v-selection-control-size: 0.875rem; + + .v-switch__thumb { + box-shadow: 0 1px 6px rgba(var(--v-shadow-key-umbra-color), var(--v-shadow-xs-opacity)); + transform: scale(1); + } + } + + .v-selection-control--dirty { + .v-selection-control__input { + transform: translateX(6px) !important; + } + } + } + + .v-label { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + line-height: 1.375rem !important; + } +} + +.v-switch.v-input, +.v-checkbox-btn, +.v-radio-btn, +.v-radio { + --v-input-control-height: auto; + + flex: unset; +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_table.scss b/website/resources/styles/@core/template/libs/vuetify/components/_table.scss new file mode 100644 index 0000000..6a45086 --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_table.scss @@ -0,0 +1,48 @@ +// 👉 Table +.v-table { + th { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important; + font-size: 0.8125rem; + letter-spacing: 0.2px; + text-transform: uppercase; + + .v-data-table-header__content { + display: flex; + justify-content: space-between; + } + } +} + +// 👉 Datatable +.v-data-table, +.v-table { + table { + thead, + tbody { + tr { + th, + td { + &:first-child:has(.v-checkbox-btn) { + padding-inline-start: 13px !important; + } + + &:first-child { + padding-inline-start: 24px !important; + } + + &:last-child { + padding-inline-end: 24px !important; + } + } + } + } + + tbody { + .v-data-table-group-header-row { + td { + background-color: rgb(var(--v-theme-surface)); + } + } + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_tabs.scss b/website/resources/styles/@core/template/libs/vuetify/components/_tabs.scss new file mode 100644 index 0000000..6e324ab --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_tabs.scss @@ -0,0 +1,91 @@ +@use "@configured-variables" as variables; +@use "@core-scss/template/mixins" as templateMixins; + +// 👉 Tabs +.v-tabs { + &.v-tabs--vertical { + --v-tabs-height: 38px !important; + + &:not(.v-tabs-pill) { + block-size: 100%; + border-inline-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + } + } + + &.v-tabs--horizontal:not(.v-tabs-pill) { + border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + + .v-tab__slider { + block-size: 3px; + } + } + + /* stylelint-disable-next-line no-descending-specificity */ + .v-btn { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + transform: none !important; + + .v-icon { + block-size: 1.125rem !important; + font-size: 1.125rem !important; + inline-size: 1.125rem !important; + } + + &:hover:not(.v-tab--selected) { + color: rgb(var(--v-theme-primary)); + + .v-btn__content { + .v-tab__slider { + opacity: var(--v-activated-opacity); + } + } + } + + &.v-btn--stacked { + /* stylelint-disable-next-line no-descending-specificity */ + .v-icon { + block-size: 1.5rem !important; + font-size: 1.5rem !important; + inline-size: 1.5rem !important; + } + } + + /* stylelint-disable-next-line no-descending-specificity */ + .v-btn__overlay, + .v-ripple__container { + opacity: 0 !important; + } + + /* stylelint-disable-next-line no-descending-specificity */ + .v-tab__slider { + inset-inline-end: 0; + inset-inline-start: unset; + } + } +} + +// 👉 Tab Pill +.v-tabs.v-tabs-pill { + .v-slide-group__content { + gap: 0.25rem; + } + + @each $color-name in variables.$theme-colors-name { + .v-tab--selected.text-#{$color-name} { + @include templateMixins.custom-elevation(var(--v-theme-#{$color-name}), "sm"); + } + } + + &.v-slide-group, + .v-slide-group__container { + box-sizing: content-box; + padding: 1rem; + margin: -1rem; + } + + .v-tab.v-btn:not(.v-tab--selected) { + &:hover { + background-color: rgba(var(--v-theme-primary), var(--v-activated-opacity)); + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_textarea.scss b/website/resources/styles/@core/template/libs/vuetify/components/_textarea.scss new file mode 100644 index 0000000..acef60a --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_textarea.scss @@ -0,0 +1,9 @@ +.v-textarea { + textarea { + opacity: 0 !important; + } + + & .v-field--active textarea { + opacity: 1 !important; + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_timeline.scss b/website/resources/styles/@core/template/libs/vuetify/components/_timeline.scss new file mode 100644 index 0000000..e10f031 --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_timeline.scss @@ -0,0 +1,99 @@ +@use "@configured-variables" as variables; + +// 👉 Timeline +.v-timeline { + // timeline items + .v-timeline-item { + &:not(:last-child) { + .v-timeline-item__body { + margin-block-end: 0.5rem; + } + } + + .app-timeline-title { + line-height: 1.375rem; + } + + .app-timeline-meta { + font-size: 0.8125rem; + font-weight: 400; + letter-spacing: 0.025rem; + line-height: 1.125rem; + } + + .app-timeline-text { + font-size: 0.9375rem; + font-weight: 400; + line-height: 1.375rem; + } + } + + // timeline icon only + &.v-timeline-icon-only { + .v-timeline-divider__dot { + .v-timeline-divider__inner-dot { + background: rgb(var(--v-theme-background)); + box-shadow: none; + } + } + } + + &:not(.v-timeline--variant-outlined) .v-timeline-divider__dot { + background: none !important; + + .v-timeline-divider__inner-dot { + box-shadow: 0 0 0 0.1875rem rgb(var(--v-theme-on-surface-variant)); + + @each $color-name in variables.$theme-colors-name { + + &.bg-#{$color-name} { + box-shadow: 0 0 0 0.1875rem rgba(var(--v-theme-#{$color-name}), 0.12); + } + } + } + } + + &.v-timeline--variant-outlined { + .v-timeline-item { + .v-timeline-divider { + .v-timeline-divider__dot { + background: none !important; + } + } + + .v-timeline-divider__after { + border: 1.5px dashed rgba(var(--v-border-color), var(--v-border-opacity)); + background: none; + } + + .v-timeline-divider__before { + background: none; + } + } + } + + // we have to override the default bg-color of the timeline dot in the card + .v-card:not(.v-card--variant-text, .v-card--variant-plain, .v-card--variant-outlined) & { + &.v-timeline-icon-only { + .v-timeline-divider__dot { + .v-timeline-divider__inner-dot { + /* stylelint-disable-next-line no-descending-specificity */ + background: rgb(var(--v-theme-surface)); + } + } + } + } + + .v-card.v-card--variant-tonal & { + &.v-timeline-icon-only { + .v-timeline-divider__dot { + .v-timeline-divider__inner-dot { + /* stylelint-disable-next-line no-descending-specificity */ + .v-icon { + background: none; + } + } + } + } + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/_tooltip.scss b/website/resources/styles/@core/template/libs/vuetify/components/_tooltip.scss new file mode 100644 index 0000000..4ec8e41 --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/_tooltip.scss @@ -0,0 +1,6 @@ +// 👉 Tooltip +.v-tooltip { + .v-overlay__content { + font-weight: 500; + } +} diff --git a/website/resources/styles/@core/template/libs/vuetify/components/index.scss b/website/resources/styles/@core/template/libs/vuetify/components/index.scss new file mode 100644 index 0000000..4e9c85c --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/components/index.scss @@ -0,0 +1,25 @@ +@use "alert"; +@use "avatar"; +@use "button"; +@use "badge"; +@use "cards"; +@use "chip"; +@use "dialog"; +@use "expansion-panels"; +@use "list"; +@use "menu"; +@use "pagination"; +@use "progress"; +@use "rating"; +@use "snackbar"; +@use "slider"; +@use "table"; +@use "tabs"; +@use "timeline"; +@use "tooltip"; +@use "otp-input"; +@use "field"; +@use "checkbox"; +@use "textarea"; +@use "radio"; +@use "switch"; diff --git a/website/resources/styles/@core/template/libs/vuetify/index.scss b/website/resources/styles/@core/template/libs/vuetify/index.scss new file mode 100644 index 0000000..b36827d --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/index.scss @@ -0,0 +1,3 @@ +@use "@core-scss/base/libs/vuetify"; +@use "overrides"; +@use "components/index.scss"; diff --git a/website/resources/styles/@core/template/libs/vuetify/overrides.scss b/website/resources/styles/@core/template/libs/vuetify/overrides.scss new file mode 100644 index 0000000..076591c --- /dev/null +++ b/website/resources/styles/@core/template/libs/vuetify/overrides.scss @@ -0,0 +1,18 @@ +@use "@core-scss/base/utils"; +@use "@configured-variables" as variables; + +// 👉 Body + +// set body font size 15px +body { + font-size: 15px !important; + + // We reduced this margin to get 40px input height + .v-input--density-compact { + --v-input-chips-margin-bottom: 1px; + } +} + +.text-caption { + color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity)); +} diff --git a/website/resources/styles/@core/template/pages/page-auth.scss b/website/resources/styles/@core/template/pages/page-auth.scss new file mode 100644 index 0000000..b4c9dce --- /dev/null +++ b/website/resources/styles/@core/template/pages/page-auth.scss @@ -0,0 +1,63 @@ +.layout-blank { + .auth-wrapper { + min-block-size: 100dvh; + } + + .auth-v1-top-shape, + .auth-v1-bottom-shape { + position: absolute; + } + + .auth-footer-mask { + position: absolute; + inset-block-end: 0; + min-inline-size: 100%; + } + + .auth-card { + z-index: 1 !important; + } + + .auth-illustration { + z-index: 1; + } + + .auth-v1-top-shape { + inset-block-start: -77px; + inset-inline-start: -45px; + } + + .auth-v1-bottom-shape { + inset-block-end: -58px; + inset-inline-end: -58px; + } + + @media (min-width: 1264px), (max-width: 959px) and (min-width: 450px) { + .v-otp-input .v-otp-input__content { + gap: 1rem; + } + } +} + +@media (min-width: 960px) { + .skin--bordered { + .auth-card-v2 { + border-inline-start: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) !important; + } + } +} + +.auth-logo { + position: absolute; + z-index: 2; + inset-block-start: 2rem; + inset-inline-start: 2.3rem; +} + +.auth-title { + font-size: 1.375rem; + font-weight: 700; + letter-spacing: 0.25px; + line-height: 1.5rem; + text-transform: capitalize; +} diff --git a/website/resources/styles/styles.scss b/website/resources/styles/styles.scss new file mode 100644 index 0000000..bbabae6 --- /dev/null +++ b/website/resources/styles/styles.scss @@ -0,0 +1,35 @@ +// Import Vuexy's full Vuetify component override chain +@use "@core-scss/template/libs/vuetify"; + +// ━━━ Project-specific overrides ━━━ + +html { + scroll-behavior: smooth; +} + +html, +body, +#app { + min-height: 100vh; +} + +// Invert white logo for light backgrounds +.logo-light { + filter: brightness(0) saturate(100%); +} + +// Links +a { + text-decoration: none; +} + +// Iconify icon size +svg.iconify { + block-size: 1em; + inline-size: 1em; +} + +// Vuetify 3 paragraph margin (Vuexy convention) +p { + margin-block-end: 1rem; +} diff --git a/website/resources/styles/variables/_template.scss b/website/resources/styles/variables/_template.scss new file mode 100644 index 0000000..0d284bf --- /dev/null +++ b/website/resources/styles/variables/_template.scss @@ -0,0 +1 @@ +@forward "@core-scss/template/variables"; diff --git a/website/resources/styles/variables/_vuetify.scss b/website/resources/styles/variables/_vuetify.scss new file mode 100644 index 0000000..d5100c1 --- /dev/null +++ b/website/resources/styles/variables/_vuetify.scss @@ -0,0 +1,2 @@ +// Forward Vuexy's Vuetify variable chain +@forward "../@core/template/libs/vuetify/variables"; diff --git a/website/resources/ts/@layouts/styles/_mixins.scss b/website/resources/ts/@layouts/styles/_mixins.scss new file mode 100644 index 0000000..ed35ae4 --- /dev/null +++ b/website/resources/ts/@layouts/styles/_mixins.scss @@ -0,0 +1,30 @@ +@use "placeholders"; +@use "@configured-variables" as variables; + +@mixin rtl { + @if variables.$enable-rtl-styles { + [dir="rtl"] & { + @content; + } + } +} + +@mixin boxed-content($nest-selector: false) { + & { + @extend %boxed-content-spacing; + + @at-root { + @if $nest-selector == false { + .layout-content-width-boxed#{&} { + @extend %boxed-content; + } + } + // stylelint-disable-next-line @stylistic/indentation + @else { + .layout-content-width-boxed & { + @extend %boxed-content; + } + } + } + } +} diff --git a/website/resources/ts/@layouts/styles/_placeholders.scss b/website/resources/ts/@layouts/styles/_placeholders.scss new file mode 100644 index 0000000..0ab9f5f --- /dev/null +++ b/website/resources/ts/@layouts/styles/_placeholders.scss @@ -0,0 +1,9 @@ +// Stub — placeholders not used in our Inertia setup +%boxed-content { + max-inline-size: 1440px; + margin-inline: auto; +} + +%boxed-content-spacing { + padding-inline: 1.5rem; +} diff --git a/website/resources/ts/@layouts/styles/_variables.scss b/website/resources/ts/@layouts/styles/_variables.scss new file mode 100644 index 0000000..90d610c --- /dev/null +++ b/website/resources/ts/@layouts/styles/_variables.scss @@ -0,0 +1,29 @@ +// @use "@styles/style.scss"; + +// 👉 Vertical nav +$layout-vertical-nav-z-index: 12 !default; +$layout-vertical-nav-width: 260px !default; +$layout-vertical-nav-collapsed-width: 80px !default; +$selector-vertical-nav-mini: ".layout-vertical-nav-collapsed .layout-vertical-nav:not(:hover)"; + +// 👉 Horizontal nav +$layout-horizontal-nav-z-index: 11 !default; +$layout-horizontal-nav-navbar-height: 64px !default; + +// 👉 Navbar +$layout-vertical-nav-navbar-height: 64px !default; +$layout-vertical-nav-navbar-is-contained: true !default; +$layout-vertical-nav-layout-navbar-z-index: 11 !default; +$layout-horizontal-nav-layout-navbar-z-index: 11 !default; + +// 👉 Main content +$layout-boxed-content-width: 1440px !default; + +// 👉Footer +$layout-vertical-nav-footer-height: 56px !default; + +// 👉 Layout overlay +$layout-overlay-z-index: 11 !default; + +// 👉 RTL +$enable-rtl-styles: true !default; diff --git a/website/resources/ts/Components/FlashMessages.vue b/website/resources/ts/Components/FlashMessages.vue new file mode 100644 index 0000000..07c35c9 --- /dev/null +++ b/website/resources/ts/Components/FlashMessages.vue @@ -0,0 +1,16 @@ + + + diff --git a/website/resources/ts/Components/StatCard.vue b/website/resources/ts/Components/StatCard.vue new file mode 100644 index 0000000..b2e01f1 --- /dev/null +++ b/website/resources/ts/Components/StatCard.vue @@ -0,0 +1,26 @@ + + + diff --git a/website/resources/ts/Components/StatusChip.vue b/website/resources/ts/Components/StatusChip.vue new file mode 100644 index 0000000..d7c35f4 --- /dev/null +++ b/website/resources/ts/Components/StatusChip.vue @@ -0,0 +1,14 @@ + + + diff --git a/website/resources/ts/Components/ThemeSwitcher.vue b/website/resources/ts/Components/ThemeSwitcher.vue new file mode 100644 index 0000000..622cb82 --- /dev/null +++ b/website/resources/ts/Components/ThemeSwitcher.vue @@ -0,0 +1,15 @@ + + + diff --git a/website/resources/ts/Components/app-form-elements/AppSelect.vue b/website/resources/ts/Components/app-form-elements/AppSelect.vue new file mode 100644 index 0000000..5a992e9 --- /dev/null +++ b/website/resources/ts/Components/app-form-elements/AppSelect.vue @@ -0,0 +1,53 @@ + + + diff --git a/website/resources/ts/Components/app-form-elements/AppTextField.vue b/website/resources/ts/Components/app-form-elements/AppTextField.vue new file mode 100644 index 0000000..35c4161 --- /dev/null +++ b/website/resources/ts/Components/app-form-elements/AppTextField.vue @@ -0,0 +1,52 @@ + + + diff --git a/website/resources/ts/Components/app-form-elements/AppTextarea.vue b/website/resources/ts/Components/app-form-elements/AppTextarea.vue new file mode 100644 index 0000000..6a0637a --- /dev/null +++ b/website/resources/ts/Components/app-form-elements/AppTextarea.vue @@ -0,0 +1,51 @@ + + + diff --git a/website/resources/ts/Layouts/AccountLayout.vue b/website/resources/ts/Layouts/AccountLayout.vue new file mode 100644 index 0000000..e01a776 --- /dev/null +++ b/website/resources/ts/Layouts/AccountLayout.vue @@ -0,0 +1,104 @@ + + + diff --git a/website/resources/ts/Layouts/AdminLayout.vue b/website/resources/ts/Layouts/AdminLayout.vue new file mode 100644 index 0000000..f7d8dc8 --- /dev/null +++ b/website/resources/ts/Layouts/AdminLayout.vue @@ -0,0 +1,120 @@ + + + diff --git a/website/resources/ts/Layouts/AuthLayout.vue b/website/resources/ts/Layouts/AuthLayout.vue new file mode 100644 index 0000000..069524e --- /dev/null +++ b/website/resources/ts/Layouts/AuthLayout.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/website/resources/ts/Layouts/MarketingLayout.vue b/website/resources/ts/Layouts/MarketingLayout.vue new file mode 100644 index 0000000..341c431 --- /dev/null +++ b/website/resources/ts/Layouts/MarketingLayout.vue @@ -0,0 +1,325 @@ + + + + + + + diff --git a/website/resources/ts/Pages/Admin/Dashboard.vue b/website/resources/ts/Pages/Admin/Dashboard.vue new file mode 100644 index 0000000..9937995 --- /dev/null +++ b/website/resources/ts/Pages/Admin/Dashboard.vue @@ -0,0 +1,49 @@ + + + diff --git a/website/resources/ts/Pages/Auth/ConfirmPassword.vue b/website/resources/ts/Pages/Auth/ConfirmPassword.vue new file mode 100644 index 0000000..00a25fa --- /dev/null +++ b/website/resources/ts/Pages/Auth/ConfirmPassword.vue @@ -0,0 +1,57 @@ + + + diff --git a/website/resources/ts/Pages/Auth/ForgotPassword.vue b/website/resources/ts/Pages/Auth/ForgotPassword.vue new file mode 100644 index 0000000..68cd919 --- /dev/null +++ b/website/resources/ts/Pages/Auth/ForgotPassword.vue @@ -0,0 +1,84 @@ + + + diff --git a/website/resources/ts/Pages/Auth/Login.vue b/website/resources/ts/Pages/Auth/Login.vue new file mode 100644 index 0000000..6b01d35 --- /dev/null +++ b/website/resources/ts/Pages/Auth/Login.vue @@ -0,0 +1,118 @@ + + + diff --git a/website/resources/ts/Pages/Auth/Register.vue b/website/resources/ts/Pages/Auth/Register.vue new file mode 100644 index 0000000..1d11389 --- /dev/null +++ b/website/resources/ts/Pages/Auth/Register.vue @@ -0,0 +1,129 @@ + + + diff --git a/website/resources/ts/Pages/Auth/ResetPassword.vue b/website/resources/ts/Pages/Auth/ResetPassword.vue new file mode 100644 index 0000000..eddbc07 --- /dev/null +++ b/website/resources/ts/Pages/Auth/ResetPassword.vue @@ -0,0 +1,94 @@ + + + diff --git a/website/resources/ts/Pages/Auth/TwoFactorChallenge.vue b/website/resources/ts/Pages/Auth/TwoFactorChallenge.vue new file mode 100644 index 0000000..92c91a0 --- /dev/null +++ b/website/resources/ts/Pages/Auth/TwoFactorChallenge.vue @@ -0,0 +1,93 @@ + + + diff --git a/website/resources/ts/Pages/Auth/VerifyEmail.vue b/website/resources/ts/Pages/Auth/VerifyEmail.vue new file mode 100644 index 0000000..6d33066 --- /dev/null +++ b/website/resources/ts/Pages/Auth/VerifyEmail.vue @@ -0,0 +1,51 @@ + + + diff --git a/website/resources/ts/Pages/Billing/Index.vue b/website/resources/ts/Pages/Billing/Index.vue new file mode 100644 index 0000000..49dd34d --- /dev/null +++ b/website/resources/ts/Pages/Billing/Index.vue @@ -0,0 +1,168 @@ + + + diff --git a/website/resources/ts/Pages/Billing/Invoices.vue b/website/resources/ts/Pages/Billing/Invoices.vue new file mode 100644 index 0000000..5fd3b60 --- /dev/null +++ b/website/resources/ts/Pages/Billing/Invoices.vue @@ -0,0 +1,88 @@ + + + diff --git a/website/resources/ts/Pages/Billing/Transactions.vue b/website/resources/ts/Pages/Billing/Transactions.vue new file mode 100644 index 0000000..1c56982 --- /dev/null +++ b/website/resources/ts/Pages/Billing/Transactions.vue @@ -0,0 +1,86 @@ + + + diff --git a/website/resources/ts/Pages/Checkout/Show.vue b/website/resources/ts/Pages/Checkout/Show.vue new file mode 100644 index 0000000..4cac3ef --- /dev/null +++ b/website/resources/ts/Pages/Checkout/Show.vue @@ -0,0 +1,199 @@ + + + diff --git a/website/resources/ts/Pages/Dashboard.vue b/website/resources/ts/Pages/Dashboard.vue new file mode 100644 index 0000000..5e396df --- /dev/null +++ b/website/resources/ts/Pages/Dashboard.vue @@ -0,0 +1,73 @@ + + + diff --git a/website/resources/ts/Pages/Marketing/About.vue b/website/resources/ts/Pages/Marketing/About.vue new file mode 100644 index 0000000..991f037 --- /dev/null +++ b/website/resources/ts/Pages/Marketing/About.vue @@ -0,0 +1,108 @@ + + + diff --git a/website/resources/ts/Pages/Marketing/Contact.vue b/website/resources/ts/Pages/Marketing/Contact.vue new file mode 100644 index 0000000..f9ccd96 --- /dev/null +++ b/website/resources/ts/Pages/Marketing/Contact.vue @@ -0,0 +1,153 @@ + + + diff --git a/website/resources/ts/Pages/Marketing/DedicatedServers.vue b/website/resources/ts/Pages/Marketing/DedicatedServers.vue new file mode 100644 index 0000000..2256f4b --- /dev/null +++ b/website/resources/ts/Pages/Marketing/DedicatedServers.vue @@ -0,0 +1,304 @@ + + + diff --git a/website/resources/ts/Pages/Marketing/GameServers.vue b/website/resources/ts/Pages/Marketing/GameServers.vue new file mode 100644 index 0000000..e8109bc --- /dev/null +++ b/website/resources/ts/Pages/Marketing/GameServers.vue @@ -0,0 +1,97 @@ + + + diff --git a/website/resources/ts/Pages/Marketing/Home.vue b/website/resources/ts/Pages/Marketing/Home.vue new file mode 100644 index 0000000..532d7a7 --- /dev/null +++ b/website/resources/ts/Pages/Marketing/Home.vue @@ -0,0 +1,113 @@ + + + diff --git a/website/resources/ts/Pages/Marketing/Pricing.vue b/website/resources/ts/Pages/Marketing/Pricing.vue new file mode 100644 index 0000000..fb82181 --- /dev/null +++ b/website/resources/ts/Pages/Marketing/Pricing.vue @@ -0,0 +1,381 @@ + + + + + + + diff --git a/website/resources/ts/Pages/Marketing/Products.vue b/website/resources/ts/Pages/Marketing/Products.vue new file mode 100644 index 0000000..fb15a12 --- /dev/null +++ b/website/resources/ts/Pages/Marketing/Products.vue @@ -0,0 +1,102 @@ + + + diff --git a/website/resources/ts/Pages/Marketing/VpsHosting.vue b/website/resources/ts/Pages/Marketing/VpsHosting.vue new file mode 100644 index 0000000..9dad23e --- /dev/null +++ b/website/resources/ts/Pages/Marketing/VpsHosting.vue @@ -0,0 +1,169 @@ + + + diff --git a/website/resources/ts/Pages/Marketing/WebHosting.vue b/website/resources/ts/Pages/Marketing/WebHosting.vue new file mode 100644 index 0000000..9532d02 --- /dev/null +++ b/website/resources/ts/Pages/Marketing/WebHosting.vue @@ -0,0 +1,253 @@ + + + diff --git a/website/resources/ts/Pages/Plans/Index.vue b/website/resources/ts/Pages/Plans/Index.vue new file mode 100644 index 0000000..403b192 --- /dev/null +++ b/website/resources/ts/Pages/Plans/Index.vue @@ -0,0 +1,92 @@ + + + diff --git a/website/resources/ts/Pages/Plans/Show.vue b/website/resources/ts/Pages/Plans/Show.vue new file mode 100644 index 0000000..18a8754 --- /dev/null +++ b/website/resources/ts/Pages/Plans/Show.vue @@ -0,0 +1,68 @@ + + + diff --git a/website/resources/ts/Pages/Profile/Show.vue b/website/resources/ts/Pages/Profile/Show.vue new file mode 100644 index 0000000..f22e319 --- /dev/null +++ b/website/resources/ts/Pages/Profile/Show.vue @@ -0,0 +1,83 @@ + + + diff --git a/website/resources/ts/Pages/Profile/TwoFactorSetup.vue b/website/resources/ts/Pages/Profile/TwoFactorSetup.vue new file mode 100644 index 0000000..e35aedd --- /dev/null +++ b/website/resources/ts/Pages/Profile/TwoFactorSetup.vue @@ -0,0 +1,149 @@ + + + diff --git a/website/resources/ts/Pages/Subscriptions/Index.vue b/website/resources/ts/Pages/Subscriptions/Index.vue new file mode 100644 index 0000000..5085b3b --- /dev/null +++ b/website/resources/ts/Pages/Subscriptions/Index.vue @@ -0,0 +1,78 @@ + + + diff --git a/website/resources/ts/Pages/Subscriptions/Show.vue b/website/resources/ts/Pages/Subscriptions/Show.vue new file mode 100644 index 0000000..8f55ed6 --- /dev/null +++ b/website/resources/ts/Pages/Subscriptions/Show.vue @@ -0,0 +1,173 @@ + + + diff --git a/website/resources/ts/app.ts b/website/resources/ts/app.ts new file mode 100644 index 0000000..2543167 --- /dev/null +++ b/website/resources/ts/app.ts @@ -0,0 +1,30 @@ +import './bootstrap' +import '@styles/styles.scss' + +import { createApp, h } from 'vue' +import { createInertiaApp } from '@inertiajs/vue3' +import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers' +import { createPinia } from 'pinia' +import installVuetify from '@/plugins/vuetify' + +const appName = import.meta.env.VITE_APP_NAME || 'EZSCALE' + +createInertiaApp({ + title: (title: string) => title ? `${title} - ${appName}` : appName, + resolve: (name: string) => resolvePageComponent( + `./Pages/${name}.vue`, + import.meta.glob('./Pages/**/*.vue'), + ), + setup({ el, App, props, plugin }) { + const app = createApp({ render: () => h(App, props) }) + + app.use(plugin) + app.use(createPinia()) + installVuetify(app) + + app.mount(el) + }, + progress: { + color: '#2563EB', + }, +}) diff --git a/website/resources/js/bootstrap.js b/website/resources/ts/bootstrap.ts similarity index 100% rename from website/resources/js/bootstrap.js rename to website/resources/ts/bootstrap.ts diff --git a/website/resources/ts/navigation/account.ts b/website/resources/ts/navigation/account.ts new file mode 100644 index 0000000..59445ca --- /dev/null +++ b/website/resources/ts/navigation/account.ts @@ -0,0 +1,14 @@ +export interface NavItem { + title: string + href: string + icon: string + matchPrefix: string +} + +export const accountNavItems: NavItem[] = [ + { title: 'Dashboard', href: '/dashboard', icon: 'tabler-smart-home', matchPrefix: '/dashboard' }, + { title: 'Subscriptions', href: '/subscriptions', icon: 'tabler-receipt', matchPrefix: '/subscriptions' }, + { title: 'Billing', href: '/billing', icon: 'tabler-credit-card', matchPrefix: '/billing' }, + { title: 'Plans', href: '/plans', icon: 'tabler-package', matchPrefix: '/plans' }, + { title: 'Profile', href: '/profile', icon: 'tabler-user', matchPrefix: '/profile' }, +] diff --git a/website/resources/ts/navigation/admin.ts b/website/resources/ts/navigation/admin.ts new file mode 100644 index 0000000..e1bcb66 --- /dev/null +++ b/website/resources/ts/navigation/admin.ts @@ -0,0 +1,5 @@ +import type { NavItem } from './account' + +export const adminNavItems: NavItem[] = [ + { title: 'Dashboard', href: '/dashboard', icon: 'tabler-smart-home', matchPrefix: '/dashboard' }, +] diff --git a/website/resources/ts/navigation/marketing.ts b/website/resources/ts/navigation/marketing.ts new file mode 100644 index 0000000..e1a8e53 --- /dev/null +++ b/website/resources/ts/navigation/marketing.ts @@ -0,0 +1,28 @@ +export interface MarketingNavItem { + title: string + href: string + children?: MarketingNavChild[] +} + +export interface MarketingNavChild { + title: string + href: string + description?: string + icon?: string +} + +export const marketingNavItems: MarketingNavItem[] = [ + { + title: 'Products', + href: '/products', + children: [ + { title: 'VPS Hosting', href: '/vps-hosting', description: 'High-performance virtual servers', icon: 'tabler-server' }, + { title: 'Dedicated Servers', href: '/dedicated-servers', description: 'Bare metal power', icon: 'tabler-server-2' }, + { title: 'Web Hosting', href: '/web-hosting', description: 'Managed web hosting', icon: 'tabler-world' }, + { title: 'Game Servers', href: '/game-servers', description: 'Low-latency game hosting', icon: 'tabler-device-gamepad-2' }, + ], + }, + { title: 'Pricing', href: '/pricing' }, + { title: 'About', href: '/about' }, + { title: 'Contact', href: '/contact' }, +] diff --git a/website/resources/ts/plugins/vuetify/defaults.ts b/website/resources/ts/plugins/vuetify/defaults.ts new file mode 100644 index 0000000..5c6150c --- /dev/null +++ b/website/resources/ts/plugins/vuetify/defaults.ts @@ -0,0 +1,128 @@ +export default { + VAlert: { + density: 'comfortable', + variant: 'tonal', + }, + VAvatar: { + variant: 'flat', + }, + VBadge: { + color: 'primary', + }, + VBtn: { + color: 'primary', + }, + VChip: { + label: true, + }, + VDataTable: { + VPagination: { + showFirstLastPage: true, + firstIcon: 'tabler-chevrons-left', + lastIcon: 'tabler-chevrons-right', + }, + }, + VExpansionPanel: { + expandIcon: 'tabler-chevron-right', + collapseIcon: 'tabler-chevron-right', + }, + VList: { + color: 'primary', + density: 'compact', + VListItem: { + ripple: false, + }, + }, + VPagination: { + density: 'comfortable', + variant: 'tonal', + }, + VTabs: { + color: 'primary', + density: 'comfortable', + VSlideGroup: { + showArrows: true, + }, + }, + VCheckbox: { + color: 'primary', + density: 'comfortable', + hideDetails: 'auto', + }, + VRadioGroup: { + color: 'primary', + density: 'comfortable', + hideDetails: 'auto', + }, + VRadio: { + density: 'comfortable', + hideDetails: 'auto', + }, + VSelect: { + variant: 'outlined', + color: 'primary', + density: 'comfortable', + hideDetails: 'auto', + }, + VTextField: { + variant: 'outlined', + density: 'comfortable', + color: 'primary', + hideDetails: 'auto', + }, + VTextarea: { + variant: 'outlined', + color: 'primary', + density: 'comfortable', + hideDetails: 'auto', + }, + VAutocomplete: { + variant: 'outlined', + color: 'primary', + density: 'comfortable', + hideDetails: 'auto', + }, + VCombobox: { + variant: 'outlined', + density: 'comfortable', + color: 'primary', + hideDetails: 'auto', + }, + VFileInput: { + variant: 'outlined', + color: 'primary', + density: 'comfortable', + hideDetails: 'auto', + }, + VSwitch: { + inset: true, + color: 'primary', + density: 'comfortable', + hideDetails: 'auto', + ripple: false, + }, + VSlider: { + color: 'primary', + thumbLabel: true, + thumbSize: 22, + trackSize: 6, + elevation: 4, + }, + VRangeSlider: { + color: 'primary', + trackSize: 6, + thumbSize: 22, + thumbLabel: true, + }, + VRating: { + color: 'warning', + }, + VProgressLinear: { + height: 6, + roundedBar: true, + rounded: true, + }, + VTooltip: { + location: 'top', + }, +} diff --git a/website/resources/ts/plugins/vuetify/icons.ts b/website/resources/ts/plugins/vuetify/icons.ts new file mode 100644 index 0000000..3cf4eb0 --- /dev/null +++ b/website/resources/ts/plugins/vuetify/icons.ts @@ -0,0 +1,53 @@ +import type { IconAliases, IconProps, IconSet } from 'vuetify' +import { h } from 'vue' +import { Icon } from '@iconify/vue' + +const aliases: Partial = { + collapse: 'tabler-chevron-up', + complete: 'tabler-check', + cancel: 'tabler-x', + close: 'tabler-x', + delete: 'tabler-circle-x-filled', + clear: 'tabler-circle-x', + success: 'tabler-circle-check', + info: 'tabler-info-circle', + warning: 'tabler-alert-triangle', + error: 'tabler-alert-circle', + prev: 'tabler-chevron-left', + next: 'tabler-chevron-right', + expand: 'tabler-chevron-down', + menu: 'tabler-menu-2', + dropdown: 'tabler-chevron-down', + sort: 'tabler-arrow-up', + sortAsc: 'tabler-arrow-up', + sortDesc: 'tabler-arrow-down', + calendar: 'tabler-calendar', + edit: 'tabler-pencil', + ratingEmpty: 'tabler-star', + ratingFull: 'tabler-star-filled', + ratingHalf: 'tabler-star-half-filled', + loading: 'tabler-refresh', + first: 'tabler-chevrons-left', + last: 'tabler-chevrons-right', + unfold: 'tabler-arrows-vertical', + file: 'tabler-paperclip', + plus: 'tabler-plus', + minus: 'tabler-minus', +} + +const iconifyIconSet: IconSet = { + component: (props: IconProps) => { + return h(Icon as unknown as string, { + icon: props.icon, + ...props, + }) + }, +} + +export const icons = { + defaultSet: 'iconify', + aliases, + sets: { + iconify: iconifyIconSet, + }, +} diff --git a/website/resources/ts/plugins/vuetify/index.ts b/website/resources/ts/plugins/vuetify/index.ts new file mode 100644 index 0000000..ec3dbc3 --- /dev/null +++ b/website/resources/ts/plugins/vuetify/index.ts @@ -0,0 +1,20 @@ +import type { App } from 'vue' +import { createVuetify } from 'vuetify' +import defaults from './defaults' +import { icons } from './icons' +import { themes } from './theme' + +import 'vuetify/styles' + +export default function installVuetify(app: App): void { + const vuetify = createVuetify({ + defaults, + icons, + theme: { + defaultTheme: 'dark', + themes, + }, + }) + + app.use(vuetify) +} diff --git a/website/resources/ts/plugins/vuetify/theme.ts b/website/resources/ts/plugins/vuetify/theme.ts new file mode 100644 index 0000000..8fea994 --- /dev/null +++ b/website/resources/ts/plugins/vuetify/theme.ts @@ -0,0 +1,80 @@ +import type { ThemeDefinition } from 'vuetify' + +export const staticPrimaryColor = '#7367F0' + +export const themes: Record = { + light: { + dark: false, + colors: { + 'primary': staticPrimaryColor, + 'on-primary': '#fff', + 'primary-darken-1': '#675DD8', + 'secondary': '#808390', + 'on-secondary': '#fff', + 'success': '#28C76F', + 'on-success': '#fff', + 'info': '#00BAD1', + 'on-info': '#fff', + 'warning': '#FF9F43', + 'on-warning': '#fff', + 'error': '#FF4C51', + 'on-error': '#fff', + 'background': '#F8F7FA', + 'on-background': '#2F2B3D', + 'surface': '#fff', + 'on-surface': '#2F2B3D', + }, + variables: { + 'overlay-scrim-background': '#2F2B3D', + 'overlay-scrim-opacity': 0.5, + 'hover-opacity': 0.06, + 'focus-opacity': 0.1, + 'selected-opacity': 0.08, + 'activated-opacity': 0.16, + 'pressed-opacity': 0.14, + 'disabled-opacity': 0.4, + 'border-color': '#2F2B3D', + 'border-opacity': 0.12, + 'high-emphasis-opacity': 0.9, + 'medium-emphasis-opacity': 0.7, + 'shadow-key-umbra-color': '#2F2B3D', + }, + }, + dark: { + dark: true, + colors: { + 'primary': staticPrimaryColor, + 'on-primary': '#fff', + 'primary-darken-1': '#675DD8', + 'secondary': '#808390', + 'on-secondary': '#fff', + 'success': '#28C76F', + 'on-success': '#fff', + 'info': '#00BAD1', + 'on-info': '#fff', + 'warning': '#FF9F43', + 'on-warning': '#fff', + 'error': '#FF4C51', + 'on-error': '#fff', + 'background': '#25293C', + 'on-background': '#E1DEF5', + 'surface': '#2F3349', + 'on-surface': '#E1DEF5', + }, + variables: { + 'overlay-scrim-background': '#171925', + 'overlay-scrim-opacity': 0.6, + 'hover-opacity': 0.06, + 'focus-opacity': 0.1, + 'selected-opacity': 0.08, + 'activated-opacity': 0.16, + 'pressed-opacity': 0.14, + 'disabled-opacity': 0.4, + 'border-color': '#E1DEF5', + 'border-opacity': 0.12, + 'high-emphasis-opacity': 0.9, + 'medium-emphasis-opacity': 0.7, + 'shadow-key-umbra-color': '#131120', + }, + }, +} diff --git a/website/resources/ts/types/index.ts b/website/resources/ts/types/index.ts new file mode 100644 index 0000000..89fa814 --- /dev/null +++ b/website/resources/ts/types/index.ts @@ -0,0 +1,99 @@ +export interface User { + id: number + name: string + email: string + status: string + two_factor_enabled?: boolean +} + +export interface AuthProps { + user: User | null +} + +export interface DomainProps { + marketing: string + account: string + admin: string +} + +export interface FlashProps { + success?: string + error?: string +} + +export interface SharedPageProps { + auth: AuthProps + flash: FlashProps + domains: DomainProps +} + +export interface Plan { + id: number + name: string + slug: string + description: string | null + price: string + billing_cycle: string + service_type: string + features: Record | null + stock_quantity: number | null + status: string + sort_order: number +} + +export interface Subscription { + id: number + type: string + stripe_status: string + gateway: string + current_period_start: string | null + current_period_end: string | null + ends_at: string | null + created_at: string + plan?: Plan +} + +export interface PaymentMethod { + id: string + brand: string + last_four: string + exp_month: number + exp_year: number + is_default: boolean +} + +export interface Invoice { + id: number + number: string + status: string + total: string + gateway: string + created_at: string +} + +export interface Transaction { + id: number + gateway: string + payment_method: string + status: string + description: string + amount: string + created_at: string +} + +export interface PaginatedResponse { + data: T[] + links: PaginationLink[] + from: number + to: number + total: number + last_page: number +} + +export interface PaginationLink { + url: string | null + label: string + active: boolean +} + +export type StatusColor = 'success' | 'error' | 'warning' | 'info' | 'secondary' diff --git a/website/resources/ts/utils/resolvers.ts b/website/resources/ts/utils/resolvers.ts new file mode 100644 index 0000000..bb1b189 --- /dev/null +++ b/website/resources/ts/utils/resolvers.ts @@ -0,0 +1,36 @@ +import type { StatusColor } from '@/types' + +export function resolveSubscriptionStatusColor(status: string): StatusColor { + const map: Record = { + active: 'success', + canceled: 'error', + past_due: 'warning', + trialing: 'info', + incomplete: 'secondary', + } + return map[status] ?? 'secondary' +} + +export function resolveInvoiceStatusColor(status: string): StatusColor { + const map: Record = { + paid: 'success', + pending: 'warning', + overdue: 'error', + } + return map[status] ?? 'secondary' +} + +export function resolveTransactionStatusColor(status: string): StatusColor { + const map: Record = { + succeeded: 'success', + failed: 'error', + pending: 'warning', + refunded: 'secondary', + } + return map[status] ?? 'secondary' +} + +export function formatPrice(price: string | number, cycle?: string): string { + const amount = parseFloat(String(price)).toFixed(2) + return cycle ? `$${amount}/${cycle}` : `$${amount}` +} diff --git a/website/resources/views/app.blade.php b/website/resources/views/app.blade.php index 3a7df29..9d4dc1a 100644 --- a/website/resources/views/app.blade.php +++ b/website/resources/views/app.blade.php @@ -7,12 +7,12 @@ {{ config('app.name', 'EZSCALE') }} - + - @vite(['resources/js/app.js']) + @vite(['resources/ts/app.ts']) @inertiaHead - + @inertia diff --git a/website/routes/marketing.php b/website/routes/marketing.php index 554b4a8..3c7a58c 100644 --- a/website/routes/marketing.php +++ b/website/routes/marketing.php @@ -2,7 +2,26 @@ declare(strict_types=1); +use App\Models\Plan; use Illuminate\Support\Facades\Route; use Inertia\Inertia; Route::get('/', fn () => Inertia::render('Marketing/Home'))->name('home'); +Route::get('/products', fn () => Inertia::render('Marketing/Products'))->name('products'); +Route::get('/vps-hosting', fn () => Inertia::render('Marketing/VpsHosting'))->name('vps-hosting'); +Route::get('/dedicated-servers', fn () => Inertia::render('Marketing/DedicatedServers'))->name('dedicated-servers'); +Route::get('/web-hosting', fn () => Inertia::render('Marketing/WebHosting'))->name('web-hosting'); +Route::get('/game-servers', fn () => Inertia::render('Marketing/GameServers'))->name('game-servers'); +Route::get('/pricing', function () { + $plans = Plan::query() + ->where('status', 'active') + ->orderBy('sort_order') + ->orderBy('price') + ->get(); + + return Inertia::render('Marketing/Pricing', [ + 'plans' => $plans, + ]); +})->name('pricing'); +Route::get('/about', fn () => Inertia::render('Marketing/About'))->name('about'); +Route::get('/contact', fn () => Inertia::render('Marketing/Contact'))->name('contact'); diff --git a/website/routes/web.php b/website/routes/web.php index eeb63f9..1ebf485 100644 --- a/website/routes/web.php +++ b/website/routes/web.php @@ -1,9 +1,3 @@ get('/') - ->assertRedirect('https://'.config('app.domains.account').'/login'); -}); diff --git a/website/tsconfig.json b/website/tsconfig.json new file mode 100644 index 0000000..fec0535 --- /dev/null +++ b/website/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "target": "esnext", + "module": "esnext", + "moduleResolution": "Bundler", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "isolatedModules": true, + "skipLibCheck": true, + "lib": ["esnext", "dom", "dom.iterable"], + "types": ["vite/client", "node"], + "paths": { + "@/*": ["./resources/ts/*"], + "@images/*": ["./resources/images/*"], + "@styles/*": ["./resources/styles/*"] + } + }, + "include": [ + "./resources/ts/**/*", + "./resources/ts/**/*.vue", + "./env.d.ts" + ], + "exclude": ["node_modules", "dist", "public"] +} diff --git a/website/vite.config.js b/website/vite.config.js deleted file mode 100644 index c7891dc..0000000 --- a/website/vite.config.js +++ /dev/null @@ -1,27 +0,0 @@ -import { defineConfig } from 'vite'; -import laravel from 'laravel-vite-plugin'; -import tailwindcss from '@tailwindcss/vite'; -import vue from '@vitejs/plugin-vue'; - -export default defineConfig({ - plugins: [ - laravel({ - input: ['resources/js/app.js'], - refresh: true, - }), - tailwindcss(), - vue({ - template: { - transformAssetUrls: { - base: null, - includeAbsolute: false, - }, - }, - }), - ], - server: { - watch: { - ignored: ['**/storage/framework/views/**'], - }, - }, -}); diff --git a/website/vite.config.ts b/website/vite.config.ts new file mode 100644 index 0000000..406077f --- /dev/null +++ b/website/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vite' +import laravel from 'laravel-vite-plugin' +import vue from '@vitejs/plugin-vue' +import vuetify from 'vite-plugin-vuetify' + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/ts/app.ts'], + refresh: true, + }), + vue({ + template: { + transformAssetUrls: { + base: null, + includeAbsolute: false, + }, + }, + }), + vuetify({ + styles: { + configFile: 'resources/styles/variables/_vuetify.scss', + }, + }), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./resources/ts', import.meta.url)), + '@images': fileURLToPath(new URL('./resources/images/', import.meta.url)), + '@styles': fileURLToPath(new URL('./resources/styles/', import.meta.url)), + '@core-scss': fileURLToPath(new URL('./resources/styles/@core', import.meta.url)), + '@configured-variables': fileURLToPath(new URL('./resources/styles/variables/_template.scss', import.meta.url)), + '@layouts': fileURLToPath(new URL('./resources/ts/@layouts', import.meta.url)), + }, + }, + server: { + watch: { + ignored: ['**/storage/framework/views/**'], + }, + }, + build: { + chunkSizeWarningLimit: 5000, + }, + optimizeDeps: { + exclude: ['vuetify'], + }, +})