first commit

This commit is contained in:
sajjadtalkhabi
2026-05-13 01:43:05 +03:30
commit d2a577f254
297 changed files with 36274 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
+2
View File
@@ -0,0 +1,2 @@
VITE_API_BASE_URL=
VITE_USE_MOCKS=true
+95
View File
@@ -0,0 +1,95 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
es2021: true,
},
plugins: ['prettier', 'unicorn', 'import'],
ignorePatterns: ['node_modules/', 'dist/', 'build/', '*.min.js'],
parserOptions: {
ecmaVersion: 2021,
sourceType: 'module',
},
extends: [
'plugin:unicorn/recommended',
'eslint:recommended',
'plugin:prettier/recommended',
'eslint-config-prettier',
'plugin:vue/vue3-essential',
],
overrides: [
{
files: ['**/*.vue'],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 2021,
sourceType: 'module',
},
},
{
files: ['**/*.cjs'],
parserOptions: {
sourceType: 'script',
},
},
],
rules: {
'no-alert': 'error',
'no-shadow': 'off',
'no-console': 'off',
'import/order': 'off',
'dot-notation': 'error',
'no-else-return': 'off',
'unicorn/no-null': 'off',
semi: ['error', 'never'],
'import/namespace': 'off',
'no-implied-eval': 'error',
'no-return-await': 'error',
'no-nested-ternary': 'off',
'no-param-reassign': 'off',
'no-throw-literal': 'error',
'consistent-return': 'off',
'no-useless-catch': 'off',
'prefer-destructuring': 'off',
'import/no-unresolved': 'off',
'no-underscore-dangle': 'off',
'unicorn/prefer-spread': 'off',
'unicorn/prefer-module': 'off',
'unicorn/filename-case': 'off',
'unicorn/no-array-reduce': 'off',
'no-constant-condition': 'error',
'unicorn/no-array-for-each': 'off',
'unicorn/prefer-node-protocol': 'off',
'import/prefer-default-export': 'off',
'unicorn/prevent-abbreviations': 'off',
'unicorn/prefer-string-replace-all': 'off',
'unicorn/consistent-function-scoping': 'off',
'unicorn/no-thenable': 'off',
'unicorn/no-static-only-class': 'off',
'unicorn/prefer-dom-node-text-content': 'off',
'unicorn/prefer-logical-operator-over-ternary': 'off',
'unicorn/prefer-number-properties': 'off',
'unicorn/prefer-ternary': 'off',
'no-unused-vars': 'off',
'no-irregular-whitespace': 'off',
'no-empty': 'off',
'import/extensions': 'off',
'vue/multi-word-component-names': 'off',
'vue/component-name-in-template-casing': 'off',
'vue/html-self-closing': 'off',
'vue/max-attributes-per-line': 'off',
'vue/html-indent': 'off',
'vue/no-unused-components': 'off',
'vue/no-reserved-component-names': 'off',
'vue/no-unused-vars': 'off',
'vue/require-v-for-key': 'off',
'vue/require-toggle-inside-transition': 'off',
'vue/require-valid-default-prop': 'off',
'vue/no-deprecated-props-default-this': 'off',
'vue/no-duplicate-attributes': 'off',
'vue/no-side-effects-in-computed-properties': 'off',
'vue/valid-v-on': 'off',
'padding-line-between-statements': 'off',
},
}
+19
View File
@@ -0,0 +1,19 @@
node_modules
dist
dist-ssr
*.local
.DS_Store
.env.local
.env.*.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
+5
View File
@@ -0,0 +1,5 @@
node_modules
dist
build
*.min.js
package-lock.json
+14
View File
@@ -0,0 +1,14 @@
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "always",
"proseWrap": "never",
"htmlWhitespaceSensitivity": "ignore",
"endOfLine": "lf"
}
+4
View File
@@ -0,0 +1,4 @@
node_modules
dist
build
*.min.css
+58
View File
@@ -0,0 +1,58 @@
{
"extends": ["stylelint-config-standard-scss", "stylelint-config-html"],
"plugins": ["stylelint-declaration-block-no-ignored-properties"],
"rules": {
"media-feature-range-notation": "prefix",
"property-no-vendor-prefix": null,
"color-function-notation": "legacy",
"declaration-block-no-redundant-longhand-properties": [
true,
{
"ignoreShorthands": ["inset", "place-content"]
}
],
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["global", "deep", "slotted"]
}
],
"at-rule-no-unknown": null,
"scss/at-rule-no-unknown": [
true,
{
"ignoreAtRules": ["use", "forward"]
}
],
"font-family-no-missing-generic-family-keyword": null,
"no-descending-specificity": null,
"plugin/declaration-block-no-ignored-properties": true,
"selector-class-pattern": [
"^[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:__[a-z0-9]+(?:-[a-z0-9]+)*)?(?:--[a-z0-9]+(?:-[a-z0-9]+)*)?$",
{
"message": "Expected class selector to follow kebab-case BEM (block__element--modifier)"
}
]
},
"overrides": [
{
"files": ["*.vue", "**/*.vue"],
"customSyntax": "postcss-html",
"rules": {
"selector-pseudo-element-no-unknown": [
true,
{
"ignorePseudoElements": ["v-deep", "v-global", "v-slotted", "deep"]
}
],
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["deep", "global", "slotted"]
}
]
}
}
],
"ignoreFiles": ["**/*.js", "**/*.ts", "node_modules/**", "dist/**", "build/**"]
}
+11
View File
@@ -0,0 +1,11 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"stylelint.vscode-stylelint",
"streetsidesoftware.code-spell-checker",
"streetsidesoftware.code-spell-checker-persian"
]
}
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Banu</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"]
}
+7850
View File
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
{
"name": "banu-front",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .js,.vue --max-warnings=0",
"lint:fix": "eslint . --ext .js,.vue --fix --max-warnings=0",
"lint:css:check": "stylelint \"src/**/*.{css,scss,vue}\" --max-warnings=0",
"lint:css:fix": "stylelint \"src/**/*.{css,scss,vue}\" --fix --max-warnings=0",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@floating-ui/dom": "^1.7.6",
"@tanstack/vue-query": "^5.62.2",
"@tanstack/vue-query-devtools": "^5.62.2",
"axios": "^1.12.2",
"lodash": "^4.18.1",
"pinia": "^3.0.3",
"vue": "^3.5.21",
"vue-advanced-cropper": "^2.8.9",
"vue-router": "^4.5.1",
"vue3-persian-datetime-picker": "^1.2.2",
"vue3-toastify": "^0.2.5",
"yup": "^1.7.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.10.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-unicorn": "^55.0.0",
"eslint-plugin-vue": "^9.15.1",
"postcss-html": "^1.8.0",
"prettier": "2.8.8",
"sass": "^1.83.0",
"stylelint": "^16.0.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-standard-scss": "^12.0.0",
"stylelint-declaration-block-no-ignored-properties": "^2.8.0",
"vite": "^7.1.6",
"vite-svg-loader": "^5.1.0",
"vue-eslint-parser": "^10.2.0"
}
}
+36
View File
@@ -0,0 +1,36 @@
<template>
<component :is="currentLayout" />
<ConfirmModal />
<VueQueryDevtools />
</template>
<script setup>
import { computed, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useRoute } from 'vue-router'
import { VueQueryDevtools } from '@tanstack/vue-query-devtools'
import ConfirmModal from '@/components/ConfirmModal.vue'
import PublicLayout from '@/layouts/PublicLayout.vue'
import AdminLayout from '@/layouts/AdminLayout.vue'
import StudentLayout from '@/layouts/StudentLayout.vue'
import { useModalStore } from '@/store/modal'
const route = useRoute()
const layouts = {
public: PublicLayout,
admin: AdminLayout,
student: StudentLayout,
}
const currentLayout = computed(() => layouts[route.meta?.layout] || PublicLayout)
const { modals } = storeToRefs(useModalStore())
watch(
() => modals.value.length,
(count) => {
document.body.style.overflow = count > 0 ? 'hidden' : ''
}
)
</script>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z"/></svg>

After

Width:  |  Height:  |  Size: 249 B

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 11 11" fill="none">
<g clip-path="url(#clip0_864_4288)">
<path d="M6.75391 3.85938H8.68359V1.92969" stroke="currentColor" stroke-width="0.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.68294 3.86055L7.54604 2.72364C6.88762 2.06525 5.9962 1.69298 5.0651 1.68756C4.13399 1.68214 3.2383 2.04401 2.57227 2.69469" stroke="currentColor" stroke-width="0.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.53711 6.43359H1.60742V8.36328" stroke="currentColor" stroke-width="0.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.60742 6.43359L2.74433 7.5705C3.40274 8.22889 4.29417 8.60116 5.22527 8.60658C6.15637 8.612 7.05207 8.25013 7.7181 7.59945" stroke="currentColor" stroke-width="0.6" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_864_4288">
<rect width="10.2917" height="10.2917" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1012 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="36" height="35" viewBox="0 0 36 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.1332 22.2004C25.1332 24.2438 24.4217 25.9842 22.9987 27.4214C21.5755 28.8584 19.8222 29.577 17.7388 29.577C15.6551 29.577 13.8942 28.8581 12.4561 27.4203C11.0179 25.9828 10.2988 24.24 10.2988 22.1918V10.0032C10.2988 8.59544 10.79 7.39693 11.7724 6.40769C12.7545 5.41845 13.9646 4.92383 15.4027 4.92383C16.8348 4.92383 18.0434 5.42072 19.0287 6.41451C20.014 7.40831 20.5066 8.60646 20.5066 10.009V21.5449C20.5066 22.2996 20.2431 22.9495 19.7161 23.4945C19.1893 24.0396 18.5375 24.3121 17.7608 24.3121C16.9843 24.3121 16.3207 24.0458 15.7699 23.5132C15.2191 22.9806 14.9438 22.3245 14.9438 21.5449V10.6379C14.9438 10.521 14.9871 10.4169 15.0737 10.3256C15.1601 10.2343 15.2703 10.1887 15.4042 10.1887C15.5379 10.1887 15.6506 10.2343 15.7424 10.3256C15.8342 10.4169 15.8801 10.521 15.8801 10.6379V21.5449C15.8801 22.06 16.0583 22.4973 16.4147 22.8566C16.7709 23.216 17.2071 23.3957 17.7234 23.3957C18.2448 23.3957 18.6829 23.2143 19.0379 22.8516C19.3928 22.4889 19.5703 22.0533 19.5703 21.5449V9.97305C19.5703 8.82305 19.1704 7.84674 18.3707 7.04414C17.5707 6.24154 16.5814 5.84023 15.4027 5.84023C14.2358 5.84023 13.2494 6.24753 12.4436 7.06211C11.638 7.87669 11.2352 8.85898 11.2352 10.009V22.1839C11.2352 23.9741 11.8655 25.501 13.1262 26.7648C14.3868 28.0286 15.9281 28.6605 17.7498 28.6605C19.5456 28.6605 21.0721 28.0286 22.3294 26.7648C23.5866 25.501 24.2091 23.9767 24.1969 22.1918V10.6379C24.1969 10.521 24.2433 10.4169 24.336 10.3256C24.4286 10.2343 24.5387 10.1887 24.6665 10.1887C24.794 10.1887 24.9037 10.2343 24.9955 10.3256C25.0873 10.4169 25.1332 10.521 25.1332 10.6379V22.2004Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M221.8,175.94C216.25,166.38,208,139.33,208,104a80,80,0,1,0-160,0c0,35.34-8.26,62.38-13.81,71.94A16,16,0,0,0,48,200H88.81a40,40,0,0,0,78.38,0H208a16,16,0,0,0,13.8-24.06ZM128,216a24,24,0,0,1-22.62-16h45.24A24,24,0,0,1,128,216ZM48,184c7.7-13.24,16-43.92,16-80a64,64,0,1,1,128,0c0,36.05,8.28,66.73,16,80Z"/></svg>

After

Width:  |  Height:  |  Size: 401 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M208,24H72A32,32,0,0,0,40,56V224a8,8,0,0,0,8,8H192a8,8,0,0,0,0-16H56a16,16,0,0,1,16-16H208a8,8,0,0,0,8-8V32A8,8,0,0,0,208,24Zm-8,160H72a31.81,31.81,0,0,0-16,4.29V56A16,16,0,0,1,72,40H200Z"/></svg>

After

Width:  |  Height:  |  Size: 288 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM72,48v8a8,8,0,0,0,16,0V48h80v8a8,8,0,0,0,16,0V48h24V80H48V48ZM208,208H48V96H208V208Z"/></svg>

After

Width:  |  Height:  |  Size: 328 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"/></svg>

After

Width:  |  Height:  |  Size: 215 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M165.66,202.34a8,8,0,0,1-11.32,11.32l-80-80a8,8,0,0,1,0-11.32l80-80a8,8,0,0,1,11.32,11.32L91.31,128Z"/></svg>

After

Width:  |  Height:  |  Size: 201 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"/></svg>

After

Width:  |  Height:  |  Size: 220 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M132,24A100.11,100.11,0,0,0,32,124v84a16,16,0,0,0,16,16h84a100,100,0,0,0,0-200Zm0,184H48V124a84,84,0,1,1,84,84Zm12-80a12,12,0,1,1-12-12A12,12,0,0,1,144,128Zm-44,0a12,12,0,1,1-12-12A12,12,0,0,1,100,128Zm88,0a12,12,0,1,1-12-12A12,12,0,0,1,188,128Z"/></svg>

After

Width:  |  Height:  |  Size: 346 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32Zm-37.66,75.66-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z"/></svg>

After

Width:  |  Height:  |  Size: 308 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z"/></svg>

After

Width:  |  Height:  |  Size: 216 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"/></svg>

After

Width:  |  Height:  |  Size: 285 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"/></svg>

After

Width:  |  Height:  |  Size: 280 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M53.92,34.62A8,8,0,1,0,42.08,45.38L61.32,66.55C25,88.84,9.38,123.2,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208a127.11,127.11,0,0,0,52.07-10.83l21.85,24a8,8,0,1,0,11.84-10.76Zm47.33,75.84,41.67,45.85a32,32,0,0,1-41.67-45.85ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.16,133.16,0,0,1,25,128c4.69-8.79,19.66-33.39,47.35-49.38l18,19.75a48,48,0,0,0,63.66,70l14.73,16.2A112,112,0,0,1,128,192Zm6-95.43a8,8,0,0,1,3-15.72,48.16,48.16,0,0,1,38.77,42.64,8,8,0,0,1-7.22,8.71,6.39,6.39,0,0,1-.75,0,8,8,0,0,1-8-7.26A32.09,32.09,0,0,0,134,96.57Zm113.28,34.69c-.42.94-10.55,23.37-33.36,43.8a8,8,0,1,1-10.67-11.92A132.77,132.77,0,0,0,231.05,128a133.15,133.15,0,0,0-23.12-30.77C185.67,75.19,158.78,64,128,64a118.37,118.37,0,0,0-19.36,1.57A8,8,0,1,1,106,49.79,134,134,0,0,1,128,48c34.88,0,66.57,13.26,91.66,38.35,18.83,18.83,27.3,37.62,27.65,38.41A8,8,0,0,1,247.31,131.26Z"/></svg>

After

Width:  |  Height:  |  Size: 1001 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M247.31,124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57,61.26,162.88,48,128,48S61.43,61.26,36.34,86.35C17.51,105.18,9,124,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208s66.57-13.26,91.66-38.34c18.83-18.83,27.3-37.61,27.65-38.4A8,8,0,0,0,247.31,124.76ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.47,133.47,0,0,1,25,128,133.33,133.33,0,0,1,48.07,97.25C70.33,75.19,97.22,64,128,64s57.67,11.19,79.93,33.25A133.46,133.46,0,0,1,231.05,128C223.84,141.46,192.43,192,128,192Zm0-112a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Z"/></svg>

After

Width:  |  Height:  |  Size: 702 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216ZM168,136H88a8,8,0,0,0,0,16h80a8,8,0,0,0,0-16Zm0,32H88a8,8,0,0,0,0,16h80a8,8,0,0,0,0-16Z"/></svg>

After

Width:  |  Height:  |  Size: 384 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M230.6,49.53A15.81,15.81,0,0,0,216,40H40A16,16,0,0,0,28.19,66.76l.08.09L96,139.17V216a16,16,0,0,0,24.87,13.32l32-21.34A16,16,0,0,0,160,194.66V139.17l67.74-72.32.08-.09A15.8,15.8,0,0,0,230.6,49.53ZM40,56h0Zm106.18,74.58A8,8,0,0,0,144,136v58.66L112,216V136a8,8,0,0,0-2.16-5.47L40,56H216Z"/></svg>

After

Width:  |  Height:  |  Size: 386 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160ZM176,24H80A56.06,56.06,0,0,0,24,80v96a56.06,56.06,0,0,0,56,56h96a56.06,56.06,0,0,0,56-56V80A56.06,56.06,0,0,0,176,24Zm40,152a40,40,0,0,1-40,40H80a40,40,0,0,1-40-40V80A40,40,0,0,1,80,40h96a40,40,0,0,1,40,40ZM192,76a12,12,0,1,1-12-12A12,12,0,0,1,192,76Z"/></svg>

After

Width:  |  Height:  |  Size: 447 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M88,64a8,8,0,0,1,8-8H216a8,8,0,0,1,0,16H96A8,8,0,0,1,88,64Zm128,56H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Zm0,64H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16ZM44,52A12,12,0,1,0,56,64,12,12,0,0,0,44,52Zm0,64a12,12,0,1,0,12,12A12,12,0,0,0,44,116Zm0,64a12,12,0,1,0,12,12A12,12,0,0,0,44,180Z"/></svg>

After

Width:  |  Height:  |  Size: 377 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z"/></svg>

After

Width:  |  Height:  |  Size: 254 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="36" height="35" viewBox="0 0 36 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.3488 15.0762C22.7483 15.0762 23.0857 14.9392 23.3611 14.6654C23.6365 14.3918 23.7742 14.0594 23.7742 13.6681C23.7742 13.2771 23.6343 12.9469 23.3545 12.6773C23.075 12.4078 22.7353 12.273 22.3356 12.273C21.9361 12.273 21.5986 12.41 21.3232 12.6838C21.0479 12.9574 20.9102 13.2898 20.9102 13.6811C20.9102 14.0721 21.0501 14.4023 21.3299 14.6719C21.6094 14.9414 21.9491 15.0762 22.3488 15.0762ZM12.9121 15.0762C13.3116 15.0762 13.649 14.9392 13.9244 14.6654C14.1998 14.3918 14.3375 14.0594 14.3375 13.6681C14.3375 13.2771 14.1976 12.9469 13.9178 12.6773C13.6383 12.4078 13.2986 12.273 12.8989 12.273C12.4994 12.273 12.1619 12.41 11.8865 12.6838C11.6111 12.9574 11.4734 13.2898 11.4734 13.6811C11.4734 14.0721 11.6133 14.4023 11.8931 14.6719C12.1727 14.9414 12.5123 15.0762 12.9121 15.0762ZM17.6326 29.577C15.8909 29.577 14.2524 29.2533 12.7171 28.6059C11.1818 27.9583 9.84618 27.0797 8.71035 25.9699C7.57452 24.8599 6.67711 23.5562 6.01813 22.0588C5.35891 20.5614 5.0293 18.9615 5.0293 17.259C5.0293 15.5544 5.36001 13.9507 6.02144 12.4481C6.68311 10.9454 7.58088 9.63823 8.71476 8.52656C9.84888 7.4149 11.1809 6.53658 12.7109 5.89162C14.2408 5.24643 15.8755 4.92383 17.615 4.92383C19.3567 4.92383 20.9952 5.24751 22.5306 5.89486C24.0659 6.54245 25.4015 7.42112 26.5373 8.53087C27.6731 9.64086 28.5705 10.9446 29.2295 12.442C29.8887 13.9393 30.2184 15.5393 30.2184 17.2418C30.2184 18.9464 29.8876 20.5501 29.2262 22.0527C28.5645 23.5554 27.6668 24.8626 26.5329 25.9742C25.3988 27.0859 24.0667 27.9642 22.5368 28.6092C21.0068 29.2544 19.3721 29.577 17.6326 29.577ZM17.6227 28.6605C20.867 28.6605 23.6212 27.5528 25.8855 25.3374C28.1499 23.1222 29.282 20.4269 29.282 17.2515C29.282 14.0763 28.1502 11.3806 25.8866 9.16445C23.6233 6.94831 20.8694 5.84023 17.6249 5.84023C14.3807 5.84023 11.6264 6.94795 9.36211 9.16337C7.09779 11.3786 5.96562 14.0739 5.96562 17.2493C5.96562 20.4245 7.09742 23.1202 9.36101 25.3363C11.6244 27.5525 14.3783 28.6605 17.6227 28.6605ZM17.6238 23.8988C18.755 23.8988 19.7995 23.6475 20.7574 23.1449C21.715 22.642 22.5105 21.959 23.1438 21.0961C23.2703 20.8802 23.2794 20.6615 23.1709 20.4398C23.0627 20.2182 22.8757 20.1074 22.6099 20.1074H12.6521C12.3804 20.1074 12.1895 20.2182 12.0793 20.4398C11.9691 20.6615 11.9835 20.8765 12.1223 21.0849C12.7423 21.9546 13.5364 22.641 14.5046 23.1441C15.4727 23.6473 16.5125 23.8988 17.6238 23.8988Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+11
View File
@@ -0,0 +1,11 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1279_3967)">
<path d="M12.2344 10.875H6.79688" stroke="currentColor" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.11956 18.8005C4.07177 18.931 4.06515 19.073 4.10058 19.2074C4.13601 19.3418 4.2118 19.4621 4.31772 19.5521C4.42364 19.6421 4.55461 19.6975 4.69297 19.7108C4.83133 19.724 4.97043 19.6945 5.09152 19.6263L19.365 11.4624C19.4712 11.4036 19.5597 11.3175 19.6214 11.2129C19.6831 11.1083 19.7156 10.9891 19.7156 10.8677C19.7156 10.7463 19.6831 10.6271 19.6214 10.5225C19.5597 10.4179 19.4712 10.3317 19.365 10.273L5.09152 2.12945C4.97079 2.06193 4.83234 2.03285 4.69466 2.04609C4.55697 2.05934 4.4266 2.11428 4.32096 2.20357C4.21533 2.29286 4.13945 2.41226 4.10346 2.54582C4.06747 2.67938 4.07309 2.82074 4.11956 2.95102L6.79668 10.8753L4.11956 18.8005Z" stroke="currentColor" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_1279_3967">
<rect width="21.75" height="21.75" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M239.84,141.66l-92,75A8,8,0,0,1,134.27,210L156,160H80a8,8,0,0,1-5.85-13.45l112-119.81A8,8,0,0,1,200,32V96l40-1.66a8,8,0,0,1,5,14.32l-5.16,32.99ZM68,144H40a8,8,0,0,1-7.59-10.53l24-72A8,8,0,0,1,64,56h44.31a8,8,0,0,1,5.85,13.45L52.83,144H68Z"/></svg>

After

Width:  |  Height:  |  Size: 339 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M227.32,73.37,182.63,28.69a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.32,96A16,16,0,0,0,227.32,73.37ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.69,147.32,64l24-24L216,84.69Z"/></svg>

After

Width:  |  Height:  |  Size: 354 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M222.37,158.46l-47.11-21.11-.13-.06a16,16,0,0,0-15.17,1.4,8.12,8.12,0,0,0-.75.56L134.87,160c-15.42-7.49-31.34-23.29-38.83-38.51l20.78-24.71c.2-.25.39-.5.57-.77a16,16,0,0,0,1.32-15.06l0-.12L97.54,33.64a16,16,0,0,0-16.62-9.52A56.26,56.26,0,0,0,32,80c0,79.4,64.6,144,144,144a56.26,56.26,0,0,0,55.88-48.92A16,16,0,0,0,222.37,158.46ZM176,208A128.14,128.14,0,0,1,48,80,40.2,40.2,0,0,1,82.87,40a.61.61,0,0,0,0,.12l21,47L83.2,111.86a6.13,6.13,0,0,0-.57.77,16,16,0,0,0-1,15.7c9.06,18.53,27.73,37.06,46.46,46.11a16,16,0,0,0,15.75-1.14,8.44,8.44,0,0,0,.74-.56L168.89,152l47,21.05h.11A40.21,40.21,0,0,1,176,208Z"/></svg>

After

Width:  |  Height:  |  Size: 700 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"/></svg>

After

Width:  |  Height:  |  Size: 213 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M12.75 3V6C12.75 6.19891 12.671 6.38968 12.5303 6.53033C12.3897 6.67098 12.1989 6.75 12 6.75C11.8011 6.75 11.6103 6.67098 11.4697 6.53033C11.329 6.38968 11.25 6.19891 11.25 6V3C11.25 2.80109 11.329 2.61032 11.4697 2.46967C11.6103 2.32902 11.8011 2.25 12 2.25C12.1989 2.25 12.3897 2.32902 12.5303 2.46967C12.671 2.61032 12.75 2.80109 12.75 3ZM21 11.25H18C17.8011 11.25 17.6103 11.329 17.4697 11.4697C17.329 11.6103 17.25 11.8011 17.25 12C17.25 12.1989 17.329 12.3897 17.4697 12.5303C17.6103 12.671 17.8011 12.75 18 12.75H21C21.1989 12.75 21.3897 12.671 21.5303 12.5303C21.671 12.3897 21.75 12.1989 21.75 12C21.75 11.8011 21.671 11.6103 21.5303 11.4697C21.3897 11.329 21.1989 11.25 21 11.25ZM16.7728 15.7125C16.631 15.5778 16.4422 15.5038 16.2466 15.5063C16.0511 15.5088 15.8642 15.5876 15.7259 15.7259C15.5876 15.8642 15.5088 16.0511 15.5063 16.2466C15.5038 16.4422 15.5778 16.631 15.7125 16.7728L17.8331 18.8944C17.9739 19.0351 18.1647 19.1142 18.3638 19.1142C18.5628 19.1142 18.7536 19.0351 18.8944 18.8944C19.0351 18.7536 19.1142 18.5628 19.1142 18.3638C19.1142 18.1647 19.0351 17.9739 18.8944 17.8331L16.7728 15.7125ZM12 17.25C11.8011 17.25 11.6103 17.329 11.4697 17.4697C11.329 17.6103 11.25 17.8011 11.25 18V21C11.25 21.1989 11.329 21.3897 11.4697 21.5303C11.6103 21.671 11.8011 21.75 12 21.75C12.1989 21.75 12.3897 21.671 12.5303 21.5303C12.671 21.3897 12.75 21.1989 12.75 21V18C12.75 17.8011 12.671 17.6103 12.5303 17.4697C12.3897 17.329 12.1989 17.25 12 17.25ZM7.22719 15.7125L5.10562 17.8331C4.96489 17.9739 4.88583 18.1647 4.88583 18.3638C4.88583 18.5628 4.96489 18.7536 5.10562 18.8944C5.24636 19.0351 5.43723 19.1142 5.63625 19.1142C5.83527 19.1142 6.02614 19.0351 6.16687 18.8944L8.2875 16.7728C8.42221 16.631 8.49621 16.4422 8.4937 16.2466C8.4912 16.0511 8.4124 15.8642 8.2741 15.7259C8.1358 15.5876 7.94894 15.5088 7.75337 15.5063C7.5578 15.5038 7.36898 15.5778 7.22719 15.7125ZM6.75 12C6.75 11.8011 6.67098 11.6103 6.53033 11.4697C6.38968 11.329 6.19891 11.25 6 11.25H3C2.80109 11.25 2.61032 11.329 2.46967 11.4697C2.32902 11.6103 2.25 11.8011 2.25 12C2.25 12.1989 2.32902 12.3897 2.46967 12.5303C2.61032 12.671 2.80109 12.75 3 12.75H6C6.19891 12.75 6.38968 12.671 6.53033 12.5303C6.67098 12.3897 6.75 12.1989 6.75 12ZM6.16687 5.10562C6.02614 4.96489 5.83527 4.88583 5.63625 4.88583C5.43723 4.88583 5.24636 4.96489 5.10562 5.10562C4.96489 5.24636 4.88583 5.43723 4.88583 5.63625C4.88583 5.83527 4.96489 6.02614 5.10562 6.16687L7.22719 8.2875C7.36898 8.42221 7.5578 8.49621 7.75337 8.4937C7.94894 8.4912 8.1358 8.4124 8.2741 8.2741C8.4124 8.1358 8.4912 7.94894 8.4937 7.75337C8.49621 7.5578 8.42221 7.36898 8.2875 7.22719L6.16687 5.10562Z"/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32Zm0,176H48V48H208V208Z"/></svg>

After

Width:  |  Height:  |  Size: 217 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M236.88,26.19a9,9,0,0,0-9.16-1.57L25.06,103.93a14.22,14.22,0,0,0,2.43,27.21L80,141.45V200a15.92,15.92,0,0,0,10,14.83,15.91,15.91,0,0,0,17.51-3.73l25.32-26.26L173,220a15.88,15.88,0,0,0,10.51,4,16.3,16.3,0,0,0,5-.79,15.85,15.85,0,0,0,10.67-11.63L239.77,35A9,9,0,0,0,236.88,26.19ZM182.42,144.5l-31.61-27.88a8,8,0,0,0-10.55,12L171.66,156,96,232V141.45a8,8,0,0,0-1.49-4.65L41.41,93.18l175.32-67.06Z"/></svg>

After

Width:  |  Height:  |  Size: 494 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"/></svg>

After

Width:  |  Height:  |  Size: 392 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H80a8,8,0,0,1,0,16H32v64H224V136H176a8,8,0,0,1,0-16h48A16,16,0,0,1,240,136ZM85.66,69.66,120,35.31V128a8,8,0,0,0,16,0V35.31l34.34,34.35a8,8,0,0,0,11.32-11.32l-48-48a8,8,0,0,0-11.32,0l-48,48A8,8,0,0,0,85.66,69.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"/></svg>

After

Width:  |  Height:  |  Size: 431 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M230.92,212c-15.23-26.33-38.7-45.21-66.09-54.16a72,72,0,1,0-73.66,0C63.78,166.78,40.31,185.66,25.08,212a8,8,0,1,0,13.85,8c18.84-32.56,52.14-52,89.07-52s70.23,19.44,89.07,52a8,8,0,1,0,13.85-8ZM72,96a56,56,0,1,1,56,56A56.06,56.06,0,0,1,72,96Z"/></svg>

After

Width:  |  Height:  |  Size: 341 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M244.8,150.4a8,8,0,0,1-11.2-1.6A51.6,51.6,0,0,0,192,128a8,8,0,0,1-7.37-4.89,8,8,0,0,1,0-6.22A8,8,0,0,1,192,112a24,24,0,1,0-23.24-30,8,8,0,1,1-15.5-4A40,40,0,1,1,219,117.51a67.94,67.94,0,0,1,27.43,21.68A8,8,0,0,1,244.8,150.4ZM190.92,212a8,8,0,1,1-13.84,8,57,57,0,0,0-98.16,0,8,8,0,1,1-13.84-8,72.06,72.06,0,0,1,33.74-29.92,48,48,0,1,1,58.36,0A72.06,72.06,0,0,1,190.92,212ZM128,176a32,32,0,1,0-32-32A32,32,0,0,0,128,176ZM72,120a8,8,0,0,0-8-8A24,24,0,1,1,87.24,82a8,8,0,1,0,15.5-4A40,40,0,1,0,37,117.51,67.94,67.94,0,0,0,9.6,139.19a8,8,0,1,0,12.8,9.61A51.6,51.6,0,0,1,64,128,8,8,0,0,0,72,120Z"/></svg>

After

Width:  |  Height:  |  Size: 690 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M117.25,157.92a60,60,0,1,0-66.5,0A95.83,95.83,0,0,0,3.53,195.63a8,8,0,1,0,13.4,8.74,80,80,0,0,1,134.14,0,8,8,0,0,0,13.4-8.74A95.83,95.83,0,0,0,117.25,157.92ZM40,108a44,44,0,1,1,44,44A44.05,44.05,0,0,1,40,108Zm210.14,98.7a8,8,0,0,1-11.07-2.33A79.83,79.83,0,0,0,172,168a8,8,0,0,1,0-16,44,44,0,1,0-16.34-84.87,8,8,0,1,1-5.94-14.85,60,60,0,0,1,55.53,105.64,95.83,95.83,0,0,1,47.22,37.71A8,8,0,0,1,250.14,206.7Z"/></svg>

After

Width:  |  Height:  |  Size: 507 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm-8-80V80a8,8,0,0,1,16,0v56a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,172Z"/></svg>

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1024 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+7
View File
@@ -0,0 +1,7 @@
@use './styles/colors';
@use './styles/variables';
@use './styles/reset';
@use './styles/typography';
@use './styles/forms';
@use './styles/modals';
@use './styles/datepicker';
+25
View File
@@ -0,0 +1,25 @@
$primary: #f36675;
$secondary: #007074;
$error: #cc2831;
$validate: #a2eaa8;
$prim-gray: #818181;
$sec-gray: #5d5d5d;
$thd-gray: #ddd;
$prim-text: #000;
$body-text: #5f5f5f;
$body-bg: #fff;
$scrollbar-thumb: #d1d5db;
$scrollbar-thumb-hover: #9ca3af;
:root {
--color-primary: #{$primary};
--color-secondary: #{$secondary};
--color-error: #{$error};
--color-validate: #{$validate};
--color-prim-gray: #{$prim-gray};
--color-sec-gray: #{$sec-gray};
--color-thd-gray: #{$thd-gray};
--color-prim-text: #{$prim-text};
--text-color: #{$body-text};
--bg-color: #{$body-bg};
}
+109
View File
@@ -0,0 +1,109 @@
.date-picker-dropdown {
.vpd-main .vpd-input-group {
display: none;
}
.vpd-wrapper .vpd-container {
margin: 0 !important;
font-family: iran-yekan;
width: 100%;
}
.vpd-wrapper .vpd-content {
width: auto;
border-radius: 8px;
}
.vpd-wrapper .vpd-header .vpd-date {
display: none;
}
.vpd-wrapper .vpd-header {
background-color: transparent !important;
padding: 5px;
}
.vpd-wrapper .vpd-header .vpd-year-label {
color: #5f5f5f;
text-align: center;
}
.vpd-wrapper .vpd-body {
overflow-x: hidden;
}
.vpd-wrapper .vpd-body .vpd-controls button {
display: flex;
align-items: center;
justify-content: center;
}
.vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-clearfix.vpd-week {
display: flex;
justify-content: space-between;
align-items: center;
}
.vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-clearfix.vpd-week .vpd-weekday {
width: auto;
float: none;
clear: both;
padding: 0 12px;
}
@media screen and (max-width: 400px) {
.vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-clearfix.vpd-week,
.vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-days {
padding: 0 5px;
}
}
.vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-days .direction-prev .vpd-clearfix {
display: flex;
justify-content: space-between;
align-items: center;
}
.vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-days {
height: fit-content !important;
padding: 0 12px;
}
.vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-days .vpd-day {
width: 35px;
height: 35px;
float: right;
line-height: 35px;
}
.vpd-wrapper
.vpd-body
.vpd-clearfix.vpd-month
.vpd-days
.vpd-day:not([disabled='true']).vpd-selected
.vpd-day-effect,
.vpd-wrapper
.vpd-body
.vpd-clearfix.vpd-month
.vpd-days
.vpd-day:not([disabled='true']):hover
.vpd-day-effect {
background-color: #f36675 !important;
}
.vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-days .vpd-day .vpd-day-effect {
width: 35px;
height: 35px;
top: -2px;
left: 0;
}
.vpd-wrapper .vpd-body .vpd-close-addon {
background-color: #f5f5f5;
border-radius: 5px;
}
.vpd-wrapper .vpd-body .vpd-month-label {
width: auto;
}
}
+20
View File
@@ -0,0 +1,20 @@
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
20%,
80% {
transform: translateX(-2px);
}
40%,
60% {
transform: translateX(2px);
}
}
.animate-shake {
animation: shake 0.5s ease-in-out;
}
+24
View File
@@ -0,0 +1,24 @@
.slide-drawer-enter-active,
.slide-drawer-leave-active {
transition: transform 0.3s ease;
}
.slide-drawer-enter-from,
.slide-drawer-leave-to {
transform: translateX(100%);
}
.slide-drawer-enter-to,
.slide-drawer-leave-from {
transform: translateX(0);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
+58
View File
@@ -0,0 +1,58 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
margin: 0 !important;
padding: 0 !important;
font-family: var(--font-family-primary), sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
overflow-x: hidden;
}
.dir-rtl {
direction: rtl;
}
.scrollbar-thin {
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
scrollbar-color: #d1d5db transparent;
}
.scrollbar-thin::-webkit-scrollbar {
width: 3px;
height: 3px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
border-radius: 10px;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: #d1d5db;
border-radius: 10px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
transform-origin: center;
}
+87
View File
@@ -0,0 +1,87 @@
@font-face {
font-family: montserrat;
src: url('../fonts/montserrat/Montserrat-ExtraLight.ttf') format('truetype');
font-weight: 200;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: montserrat;
src: url('../fonts/montserrat/Montserrat-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: aria;
src: url('../fonts/aria/AriaWebFaNum-Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: aria;
src: url('../fonts/aria/AriaWebFaNum-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: aria;
src: url('../fonts/aria/AriaWebFaNum-Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: aria;
src: url('../fonts/aria/AriaWebFaNum-SemiBold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: aria;
src: url('../fonts/aria/AriaWeb-Normal.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: iran-yekan;
src: url('../fonts/iran-yekan/iranyekanwebregularfanum.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: iran-yekan;
src: url('../fonts/iran-yekan/iranyekanwebmediumfanum.woff') format('woff');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: iran-yekan;
src: url('../fonts/iran-yekan/iranyekanwebboldfanum.woff') format('woff');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: iran-yekan;
src: url('../fonts/iran-yekan/iranyekanweblightfanum.woff') format('woff');
font-weight: 300;
font-style: normal;
font-display: swap;
}
+36
View File
@@ -0,0 +1,36 @@
$font-fa: 'aria', 'Tahoma', sans-serif;
$font-number-fa: 'iran-yekan', 'aria', sans-serif;
$font-en: 'montserrat', 'Tahoma', sans-serif;
$font-size-en-xs: 0.625rem;
$font-size-2xs: 0.6875rem;
$font-size-xxs: 0.75rem;
$font-size-label: 0.8125rem;
$font-size-input: 0.9375rem;
$font-size-xxl: 2.0625rem;
$input-height: 2.5rem;
$input-height-lg: 3.125rem;
$icon-xs: 1.0625rem;
$icon-sm: 1.375rem;
$icon-lg: 1.875rem;
$icon-xl: 2.125rem;
$icon-2xl: 3.375rem;
:root {
--font-family-primary: 'aria';
--font-family-fa: #{$font-fa};
--font-family-number-fa: #{$font-number-fa};
--font-family-en: #{$font-en};
--font-size-en-xs: #{$font-size-en-xs};
--font-size-2xs: #{$font-size-2xs};
--font-size-xxs: #{$font-size-xxs};
--font-size-label: #{$font-size-label};
--font-size-input: #{$font-size-input};
--font-size-xxl: #{$font-size-xxl};
--input-height: #{$input-height};
--input-height-lg: #{$input-height-lg};
--icon-xs: #{$icon-xs};
--icon-sm: #{$icon-sm};
--icon-lg: #{$icon-lg};
--icon-xl: #{$icon-xl};
--icon-2xl: #{$icon-2xl};
}
+125
View File
@@ -0,0 +1,125 @@
<template>
<RouterLink v-if="to" :to="to" :class="rootClass">
<slot name="prependIcon" />
<span :class="['base-button__text', textClass]">{{ text }}</span>
<slot name="appendIcon" />
</RouterLink>
<button
v-else
:class="rootClass"
:type="type"
:disabled="disabled || loading"
@click="handleClick"
>
<template v-if="loading">
<SvgIcon
name="spinner"
:size="24"
:color="spinnerFill"
class="base-button__spinner animate-spin"
/>
</template>
<template v-else>
<slot name="prependIcon" />
<span :class="['base-button__text', textClass]">{{ text }}</span>
<slot name="appendIcon" />
</template>
</button>
</template>
<script setup>
import { computed } from 'vue'
import { RouterLink } from 'vue-router'
import SvgIcon from '@/components/icons/SvgIcon.vue'
const props = defineProps({
to: { type: [String, Object], default: null },
type: { type: String, default: 'button' },
variant: { type: String, default: 'primary' },
text: { type: String, default: 'تایید' },
textClass: { type: String, default: '' },
customClass: { type: String, default: 'base-button--full' },
disabled: { type: Boolean, default: false },
loading: { type: Boolean, default: false },
})
const emit = defineEmits(['click'])
const rootClass = computed(() => [
'base-button',
`base-button--${props.variant}`,
props.customClass,
{ 'base-button--disabled': props.disabled, 'base-button--loading': props.loading },
])
const spinnerFill = computed(() => (props.variant === 'primary' ? '#fff' : '#2B2B2B'))
const handleClick = (event) => {
if (props.type !== 'submit' && (props.disabled || props.loading)) {
event.preventDefault()
event.stopPropagation()
return
}
if (props.type === 'submit') return
emit('click', event)
}
</script>
<style lang="scss" scoped>
.base-button {
display: flex;
align-items: center;
justify-content: center;
height: 2.35rem;
border: none;
border-radius: 9999px;
cursor: pointer;
transition: opacity 0.2s ease, background-color 0.2s ease;
font-family: var(--font-family-fa);
text-decoration: none;
padding: 0 1rem;
&__text {
line-height: 1.25rem;
font-size: 0.875rem;
}
&__spinner {
margin: 0 auto;
}
&--full {
width: 100%;
}
&--primary {
background-color: var(--color-primary);
color: #fff;
&:hover:not(.base-button--disabled, .base-button--loading) {
opacity: 0.9;
}
}
&--secondary {
background-color: var(--color-thd-gray);
color: var(--color-sec-gray);
&:hover:not(.base-button--disabled, .base-button--loading) {
opacity: 0.9;
}
}
&--transparent {
background-color: transparent;
color: var(--color-sec-gray);
}
&--disabled,
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
</style>
+171
View File
@@ -0,0 +1,171 @@
<template>
<Teleport to="body">
<Transition name="fade-scale" @after-enter="onOpened" @after-leave="onClosed">
<div v-if="isOpen" class="basic-modal" @click="onBackdropClick">
<div class="basic-modal__container" :style="containerStyle" @click.stop>
<div class="basic-modal__card scrollbar-thin">
<header v-if="hasHeader" class="basic-modal__header">
<slot name="header">
<LineTitleBlock v-if="title || titleEn" :title="title" :title-en="titleEn" />
<span v-else />
</slot>
<button
v-if="showCloseButton"
type="button"
class="basic-modal__close"
aria-label="بستن"
@click="close"
>
<SvgIcon name="close" :size="36" color="#B1B1B1" />
</button>
</header>
<slot :data="modalData" :close="close" />
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
import { computed, getCurrentInstance, onBeforeUnmount, useSlots, watch } from 'vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { useModalStore } from '@/store/modal'
const props = defineProps({
name: { type: String, default: '' },
title: { type: String, default: '' },
titleEn: { type: String, default: '' },
closable: { type: Boolean, default: true },
closeOnEsc: { type: Boolean, default: true },
showCloseButton: { type: Boolean, default: true },
width: { type: String, default: '' },
maxWidth: { type: String, default: '98%' },
minWidth: { type: String, default: '85%' },
})
const emit = defineEmits(['open', 'close'])
const instance = getCurrentInstance()
const slots = useSlots()
const { isModal, getModal, closeModal } = useModalStore()
const modalName = computed(() => props.name || instance?.parent?.type?.name || '')
const isOpen = computed(() => !!modalName.value && isModal(modalName.value))
const modalData = computed(() => getModal(modalName.value)?.data)
const hasHeader = computed(
() => props.showCloseButton || !!slots.header || !!props.title || !!props.titleEn
)
const containerStyle = computed(() => {
const style = { maxWidth: props.maxWidth, minWidth: props.minWidth }
if (props.width) style.width = props.width
return style
})
const close = () => closeModal(modalName.value)
const onBackdropClick = () => {
if (props.closable) close()
}
const onOpened = () => emit('open', modalData.value)
const onClosed = () => emit('close')
const onKeydown = (e) => {
if (e.key === 'Escape' && props.closeOnEsc && props.closable && isOpen.value) {
e.preventDefault()
close()
}
}
watch(
isOpen,
(open) => {
if (open) {
window.addEventListener('keydown', onKeydown)
} else {
window.removeEventListener('keydown', onKeydown)
}
},
{ immediate: true }
)
onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
</script>
<style lang="scss" scoped>
.basic-modal {
position: fixed;
inset: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 5%);
backdrop-filter: blur(12px);
&__container {
background: transparent;
border-radius: 1.5rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow-x: hidden;
position: relative;
transition: all 0.2s ease;
max-height: calc(100dvh - 10%);
}
&__card {
width: 100%;
padding: 1rem 1.25rem 1.5rem;
background: #fff;
border-radius: 1.5rem;
overflow-y: auto;
position: relative;
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
&__close {
height: 2.75rem;
width: 2.75rem;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
cursor: pointer;
border-radius: 9999px;
flex-shrink: 0;
&:hover {
opacity: 0.7;
}
}
}
.fade-scale-enter-active,
.fade-scale-leave-active {
transition: all 0.25s ease;
}
.fade-scale-enter-from,
.fade-scale-leave-to {
opacity: 0;
transform: scale(0.95);
}
</style>
+201
View File
@@ -0,0 +1,201 @@
<template>
<div
class="circle-button-wrap"
@mouseenter="onShow"
@mouseleave="onHide"
@focusin="onShow"
@focusout="onHide"
>
<button
ref="triggerRef"
:type="type"
class="circle-button"
:style="buttonStyle"
:disabled="disabled || loading"
@click="handleClick"
>
<slot name="icon" />
</button>
<Teleport to="body">
<Transition name="tooltip-fade">
<div
v-if="showTooltip && tooltip"
ref="tooltipRef"
class="circle-button__tooltip"
:class="`circle-button__tooltip--${actualPlacement}`"
:style="tooltipStyle"
role="tooltip"
>
{{ tooltip }}
<span
class="circle-button__tooltip-arrow"
:style="arrowStyle"
/>
</div>
</Transition>
</Teleport>
</div>
</template>
<script setup>
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { arrow, autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'
const props = defineProps({
type: { type: String, default: 'button' },
tooltip: { type: String, default: '' },
tooltipPosition: {
type: String,
default: 'top',
validator: (v) => ['top', 'bottom'].includes(v),
},
bgColor: { type: String, default: '#fff' },
size: { type: String, default: '2.75rem' },
disabled: { type: Boolean, default: false },
loading: { type: Boolean, default: false },
})
const emit = defineEmits(['click'])
const showTooltip = ref(false)
const triggerRef = ref(null)
const tooltipRef = ref(null)
const arrowEl = ref(null)
const tooltipStyle = ref({ position: 'fixed', top: '0', left: '0' })
const arrowStyle = ref({})
const actualPlacement = ref(props.tooltipPosition)
const buttonStyle = computed(() => ({
width: props.size,
height: props.size,
backgroundColor: props.bgColor,
}))
let cleanupAutoUpdate = null
const updatePosition = async () => {
if (!triggerRef.value || !tooltipRef.value) return
arrowEl.value = tooltipRef.value.querySelector('.circle-button__tooltip-arrow')
const middleware = [offset(6), flip(), shift({ padding: 8 })]
if (arrowEl.value) middleware.push(arrow({ element: arrowEl.value }))
const result = await computePosition(triggerRef.value, tooltipRef.value, {
placement: props.tooltipPosition,
strategy: 'fixed',
middleware,
})
tooltipStyle.value = {
position: 'fixed',
top: `${result.y}px`,
left: `${result.x}px`,
}
actualPlacement.value = result.placement
const arrowData = result.middlewareData.arrow
if (arrowEl.value && arrowData) {
const side = result.placement.split('-')[0]
const oppositeSide = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' }[side]
arrowStyle.value = {
left: arrowData.x != null ? `${arrowData.x}px` : '',
top: arrowData.y != null ? `${arrowData.y}px` : '',
[oppositeSide]: '-0.25rem',
}
}
}
const startAutoUpdate = () => {
if (!triggerRef.value || !tooltipRef.value) return
cleanupAutoUpdate?.()
cleanupAutoUpdate = autoUpdate(triggerRef.value, tooltipRef.value, updatePosition)
}
const stopAutoUpdate = () => {
cleanupAutoUpdate?.()
cleanupAutoUpdate = null
}
watch(showTooltip, async (open) => {
if (open) {
await nextTick()
startAutoUpdate()
} else {
stopAutoUpdate()
}
})
onBeforeUnmount(stopAutoUpdate)
const onShow = () => {
if (!props.tooltip) return
showTooltip.value = true
}
const onHide = () => {
showTooltip.value = false
}
const handleClick = (event) => {
if (props.disabled || props.loading) {
event.preventDefault()
return
}
emit('click', event)
}
</script>
<style lang="scss" scoped>
.circle-button-wrap {
position: relative;
display: inline-block;
}
.circle-button {
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 9999px;
cursor: pointer;
padding: 0;
z-index: 20;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
</style>
<style lang="scss">
.circle-button__tooltip {
z-index: 1000;
white-space: nowrap;
padding: 0.25rem 0.625rem;
font-size: 0.75rem;
font-weight: 500;
color: #fff;
background-color: #000;
border-radius: 0.375rem;
box-shadow: 0 1px 2px rgba(0, 0, 0, 5%);
pointer-events: none;
}
.circle-button__tooltip-arrow {
position: absolute;
width: 0.5rem;
height: 0.5rem;
background-color: #000;
transform: rotate(45deg);
}
.tooltip-fade-enter-active,
.tooltip-fade-leave-active {
transition: opacity 0.15s ease;
}
.tooltip-fade-enter-from,
.tooltip-fade-leave-to {
opacity: 0;
}
</style>
+91
View File
@@ -0,0 +1,91 @@
<template>
<BasicModal min-width="0" max-width="640px" width="95%" :show-close-button="false">
<template #default="{ data }">
<div class="confirm-modal">
<h3 class="confirm-modal__title">{{ data?.title || 'تأیید حذف' }}</h3>
<div class="confirm-modal__message">
<span v-if="data?.message" v-html="data.message" />
<span v-else>این عمل غیرقابل بازگشت است. آیا از حذف اطمینان دارید؟</span>
</div>
<div class="confirm-modal__actions">
<BaseButton
variant="transparent"
text="منصرف شدم"
text-class=""
custom-class="confirm-modal__btn confirm-modal__btn--cancel"
@click="cancel(data)"
/>
<BaseButton
variant="primary"
text="تایید"
:loading="!!data?.confirmLoading"
custom-class="confirm-modal__btn confirm-modal__btn--confirm"
@click="confirm(data)"
/>
</div>
</div>
</template>
</BasicModal>
</template>
<script setup>
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import { useModalStore } from '@/store/modal'
defineOptions({ name: 'ConfirmModal' })
const { closeModal } = useModalStore()
const confirm = (data) => {
data?.onConfirm?.()
if (!data?.keepOpenOnConfirm) closeModal('ConfirmModal')
}
const cancel = (data) => {
data?.onCancel?.()
closeModal('ConfirmModal')
}
</script>
<style lang="scss" scoped>
.confirm-modal {
padding: 1.5rem;
&__title {
font-family: var(--font-family-fa);
font-weight: 700;
font-size: 1.25rem;
color: #1f2937;
text-align: center;
margin-bottom: 0.5rem;
}
&__message {
margin-bottom: 1.5rem;
font-family: var(--font-family-fa);
text-align: center;
:deep(p) {
margin: 0;
}
}
&__actions {
display: flex;
gap: 0.75rem;
justify-content: flex-end;
}
&__btn {
min-width: 100px;
padding: 0.5rem 1.5rem;
&--confirm {
padding-inline: 1.5rem 2.5rem;
}
}
}
</style>
+93
View File
@@ -0,0 +1,93 @@
<template>
<div class="copyright">
<div class="copyright__text">
<a href="https://mugweb.ir/" target="_blank" rel="noopener">
<p class="copyright__en">All Rights Reserved. 2026</p>
<p class="copyright__fa">تمام حقوق مادی و معنوی این وبسایت محفوظ هست.</p>
</a>
</div>
<div class="copyright__social">
<ul>
<li v-for="item in social" :key="item.title">
<a :href="item.link" target="_blank" rel="noopener">
<SvgIcon :name="item.icon" :size="24" color="var(--color-thd-gray)" />
</a>
</li>
</ul>
</div>
</div>
</template>
<script setup>
import SvgIcon from '@/components/icons/SvgIcon.vue'
const social = [
{
title: 'اینستاگرام',
link: 'https://www.instagram.com/mugweb.ir?igsh=MWprOHF6eWZub3k4bA==',
icon: 'instagram',
},
{
title: 'تلگرام',
link: '#',
icon: 'telegram',
},
]
</script>
<style lang="scss" scoped>
.copyright {
display: flex;
gap: 0.5rem;
justify-content: center;
align-items: center;
&__text {
padding: 0 0.25rem;
text-align: left;
direction: ltr;
a {
color: inherit;
text-decoration: none;
}
}
&__en {
font-family: var(--font-family-en);
font-size: 0.65rem;
color: #9ca3af;
margin: 0;
}
&__fa {
font-family: var(--font-family-fa);
font-size: 0.75rem;
margin: 0;
direction: rtl;
text-align: right;
}
&__social {
border-right: 1px solid var(--color-thd-gray);
padding: 0 0.5rem;
ul {
list-style: none;
display: flex;
gap: 0.5rem;
margin: 0;
padding: 0;
}
a {
display: inline-flex;
transition: opacity 0.15s;
&:hover {
opacity: 0.7;
}
}
}
}
</style>
+196
View File
@@ -0,0 +1,196 @@
<template>
<Teleport to="body">
<Transition name="dropdown">
<div v-if="modelValue" ref="floatingEl" class="dropdown-menu" :style="floatingStyle">
<ul class="dropdown-menu__list">
<li
v-for="(item, i) in items"
:key="item.key ?? item.label ?? i"
class="dropdown-menu__item-wrap"
>
<button
type="button"
class="dropdown-menu__item"
:class="{ 'dropdown-menu__item--danger': item.danger }"
:disabled="item.disabled"
@click="handleClick(item)"
>
<SvgIcon v-if="item.icon" :name="item.icon" :size="16" class="dropdown-menu__icon" />
<span class="dropdown-menu__label">{{ item.label }}</span>
</button>
</li>
</ul>
</div>
</Transition>
</Teleport>
</template>
<script setup>
import { onBeforeUnmount, ref, watch } from 'vue'
import {
autoUpdate,
computePosition,
flip,
offset as offsetMiddleware,
shift,
} from '@floating-ui/dom'
import SvgIcon from '@/components/icons/SvgIcon.vue'
const props = defineProps({
modelValue: { type: Boolean, default: false },
reference: { type: [Object, null], default: null },
items: { type: Array, default: () => [] },
placement: { type: String, default: 'bottom-end' },
offset: { type: Number, default: 8 },
minWidth: { type: String, default: '12rem' },
})
const emit = defineEmits(['update:modelValue', 'select'])
const floatingEl = ref(null)
const floatingStyle = ref({ position: 'fixed', top: '0', left: '0', minWidth: props.minWidth })
let cleanup = null
const close = () => emit('update:modelValue', false)
const mountFloating = async () => {
await Promise.resolve()
const referenceEl = props.reference
const floating = floatingEl.value
if (!referenceEl || !floating) return
cleanup = autoUpdate(referenceEl, floating, () => {
computePosition(referenceEl, floating, {
placement: props.placement,
strategy: 'fixed',
middleware: [offsetMiddleware(props.offset), flip(), shift({ padding: 8 })],
}).then(({ x, y }) => {
floatingStyle.value = {
position: 'fixed',
top: `${y}px`,
left: `${x}px`,
minWidth: props.minWidth,
}
})
})
document.addEventListener('mousedown', handleOutsideClick)
document.addEventListener('keydown', handleEsc)
}
const unmountFloating = () => {
cleanup?.()
cleanup = null
document.removeEventListener('mousedown', handleOutsideClick)
document.removeEventListener('keydown', handleEsc)
}
const handleOutsideClick = (event) => {
const referenceEl = props.reference
const floating = floatingEl.value
if (referenceEl?.contains(event.target)) return
if (floating?.contains(event.target)) return
close()
}
const handleEsc = (event) => {
if (event.key === 'Escape') close()
}
const handleClick = (item) => {
if (item.disabled) return
emit('select', item)
item.onClick?.(item)
close()
}
watch(
() => props.modelValue,
(open) => {
if (open) {
setTimeout(mountFloating, 0)
} else {
unmountFloating()
}
}
)
onBeforeUnmount(unmountFloating)
</script>
<style lang="scss" scoped>
.dropdown-menu {
z-index: 9999;
background: #fff;
border-radius: 0.875rem;
box-shadow: 0 10px 25px -8px rgba(0, 0, 0, 12%), 0 4px 10px -4px rgba(0, 0, 0, 8%);
padding: 0.25rem;
max-height: 70vh;
overflow-y: auto;
overscroll-behavior: contain;
&__list {
list-style: none;
margin: 0;
padding: 0;
}
&__item-wrap {
& + & {
border-top: 1px solid #ececec;
}
}
&__item {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.75rem;
width: 100%;
padding: 0.5rem 0.75rem;
border: none;
border-radius: 0.625rem;
background: transparent;
cursor: pointer;
font-family: var(--font-family-fa);
font-size: 0.85rem;
color: var(--color-sec-gray, #363636);
transition: background-color 0.15s ease;
&:hover {
background-color: #f7f7f7;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&--danger {
color: var(--color-error);
}
}
&__label {
flex: 1;
text-align: right;
white-space: nowrap;
}
&__icon {
flex-shrink: 0;
}
}
.dropdown-enter-active,
.dropdown-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-6px);
}
</style>
+165
View File
@@ -0,0 +1,165 @@
<template>
<div class="header-block">
<div class="header-block__row">
<div class="header-block__menu">
<button type="button" class="header-block__menu-btn" @click="ui.toggleSidebar">
<SvgIcon name="menu" :size="24" color="#9a9a9a" />
</button>
</div>
<div class="header-block__logo">
<img :src="logo" alt="logo" />
</div>
<div class="header-block__title">
<slot>
<div v-if="!hasSlotContent && pageTitle">{{ pageTitle }}</div>
</slot>
</div>
<div class="header-block__icons">
<ul>
<li v-for="item in iconList" :key="item.icon">
<button type="button" class="header-block__icon-btn" @click="item.action">
<SvgIcon :name="item.icon" :size="24" color="#9a9a9a" />
<span v-if="item.count" class="header-block__icon-badge">{{ item.count }}</span>
</button>
</li>
</ul>
</div>
</div>
<div class="header-block__title-mobile">
<slot>
<div v-if="!hasSlotContent && pageTitle">{{ pageTitle }}</div>
</slot>
</div>
</div>
</template>
<script setup>
import { computed, useSlots } from 'vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { gallery } from '@/utils/gallery'
import { useUIStore } from '@/store/ui'
defineProps({
pageTitle: { type: String, default: '' },
})
const slots = useSlots()
const hasSlotContent = computed(() => !!slots.default)
const ui = useUIStore()
const logo = gallery.authLogo
const iconList = [
{ icon: 'chat', action: () => {} },
{ icon: 'bell', action: () => {}, count: 5 },
{ icon: 'user', action: () => {} },
]
</script>
<style lang="scss" scoped>
.header-block {
&__row {
display: flex;
align-items: center;
gap: 0.625rem;
}
&__menu {
flex-basis: 33.33%;
@media (min-width: 1024px) {
display: none;
}
}
&__menu-btn {
border: none;
background: transparent;
border-radius: 9999px;
padding: 0;
cursor: pointer;
}
&__logo {
flex-basis: 33.33%;
@media (min-width: 1024px) {
display: none;
}
img {
margin: 0 auto;
height: 3.5rem;
object-fit: contain;
}
}
&__title {
display: none;
text-align: right;
font-family: var(--font-family-fa);
font-weight: 500;
font-size: 1.125rem;
@media (min-width: 1024px) {
display: block;
flex: 1 1 auto;
}
}
&__title-mobile {
margin-top: 2.5rem;
flex: 1 1 auto;
text-align: right;
font-family: var(--font-family-fa);
font-weight: 500;
font-size: 1.125rem;
@media (min-width: 1024px) {
display: none;
}
}
&__icons {
flex-basis: 33.33%;
ul {
list-style: none;
display: flex;
align-items: center;
gap: 0.625rem;
justify-content: flex-end;
margin: 0;
padding: 0;
}
}
&__icon-btn {
position: relative;
border: none;
background: transparent;
border-radius: 9999px;
padding: 0;
cursor: pointer;
}
&__icon-badge {
position: absolute;
top: -0.125rem;
right: -0.125rem;
padding-top: 0.125rem;
height: 1rem;
width: 1rem;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-primary);
color: #fff;
font-family: var(--font-family-number-fa);
font-size: 0.6875rem;
}
}
</style>
+53
View File
@@ -0,0 +1,53 @@
<template>
<div class="line-title">
<span class="line-title__bar" />
<div class="line-title__text">
<p class="line-title__fa">{{ title }}</p>
<p v-if="titleEn" class="line-title__en">{{ titleEn }}</p>
</div>
</div>
</template>
<script setup>
defineProps({
title: { type: String, default: '' },
titleEn: { type: String, default: '' },
})
</script>
<style lang="scss" scoped>
.line-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.625rem;
padding: 0.75rem 0;
&__bar {
background-color: var(--color-primary);
box-shadow: 0 1px 2px rgba(0, 0, 0, 5%);
border-radius: 1rem;
height: 1.9rem;
width: 0.375rem;
flex-shrink: 0;
}
&__text {
text-align: right;
}
&__fa {
font-family: var(--font-family-fa);
font-weight: 500;
font-size: 0.875rem;
margin: 0;
}
&__en {
font-family: var(--font-family-en);
font-weight: 300;
font-size: 0.75rem;
margin: 0;
}
}
</style>
@@ -0,0 +1,68 @@
<template>
<div class="boxed-icon-title">
<div class="boxed-icon-title__icon-wrap">
<span class="boxed-icon-title__icon-glow" />
<slot name="icon" />
</div>
<div class="boxed-icon-title__text">
<p class="boxed-icon-title__title">{{ title }}</p>
<p class="boxed-icon-title__desc">{{ desc }}</p>
</div>
</div>
</template>
<script setup>
defineProps({
title: { type: String, default: 'عنوان' },
desc: { type: String, default: 'توضیحات' },
})
</script>
<style lang="scss" scoped>
.boxed-icon-title {
display: flex;
align-items: center;
gap: 0.5rem;
&__icon-wrap {
background: #fff;
border-radius: 0.5rem;
display: flex;
justify-content: center;
align-items: center;
height: 3rem;
width: 3rem;
position: relative;
}
&__icon-glow {
position: absolute;
inset: 0;
margin: auto;
background: var(--color-primary);
filter: blur(4px);
height: 1rem;
width: 1rem;
opacity: 0.15;
border-radius: 9999px;
}
&__text {
text-align: right;
font-family: var(--font-family-fa);
}
&__title {
font-size: 1.125rem;
font-weight: 500;
margin: 0;
}
&__desc {
font-size: 0.75rem;
font-weight: 400;
color: #9ca3af;
margin: 0;
}
}
</style>
+69
View File
@@ -0,0 +1,69 @@
<template>
<div class="line-info">
<span class="line-info__bar" />
<p class="line-info__title">{{ title }}</p>
<div v-if="numericDesc !== '' || desc" class="line-info__value">
<span v-if="numericDesc !== ''" class="line-info__numeric">{{ numericDesc }}</span>
<span v-if="desc" class="line-info__desc">{{ desc }}</span>
</div>
</div>
</template>
<script setup>
defineProps({
title: { type: String, default: '' },
desc: { type: String, default: '' },
numericDesc: { type: [String, Number], default: '' },
})
</script>
<style lang="scss" scoped>
.line-info {
position: relative;
padding: 0.75rem;
text-align: start;
&__bar {
position: absolute;
inset-block-start: 50%;
inset-inline-end: 0;
transform: translateY(-50%);
background: #efefef;
border-radius: 1rem;
height: 60%;
min-height: 1.9rem;
width: 0.25rem;
right: 0;
}
&__title {
font-family: var(--font-family-fa);
font-weight: 400;
font-size: 0.875rem;
line-height: 1.5rem;
color: #b7b7b7;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__value {
display: flex;
gap: 0.5rem;
align-items: center;
font-weight: 500;
font-size: 0.875rem;
line-height: 1.75rem;
}
&__numeric {
font-family: var(--font-family-en);
}
&__desc {
font-family: var(--font-family-fa);
text-align: justify;
}
}
</style>
+49
View File
@@ -0,0 +1,49 @@
<template>
<div class="no-items">
<div class="no-items__inner">
<SvgIcon name="warning" :size="120" color="#cbd5e1" />
<p class="no-items__title">{{ title }}</p>
<p class="no-items__desc">{{ desc }}</p>
</div>
</div>
</template>
<script setup>
import SvgIcon from '@/components/icons/SvgIcon.vue'
defineProps({
title: { type: String, default: '' },
desc: { type: String, default: '' },
})
</script>
<style lang="scss" scoped>
.no-items {
display: flex;
align-items: center;
border-radius: 0.75rem;
justify-content: center;
height: 100%;
min-height: 20rem;
&__inner {
text-align: center;
}
&__title {
font-family: var(--font-family-fa);
font-weight: 300;
font-size: 1.25rem;
color: #999;
margin: 1rem 0 0.625rem;
}
&__desc {
font-family: var(--font-family-fa);
font-weight: 700;
font-size: 1.25rem;
color: #3c3c3c;
margin: 0;
}
}
</style>
+129
View File
@@ -0,0 +1,129 @@
<template>
<div v-if="visiblePages.length > 1" class="pagination">
<button
type="button"
:disabled="isFirstPage"
class="pagination__nav"
:class="{ 'pagination__nav--disabled': isFirstPage }"
@click="changePage(currentPage - 1)"
>
<SvgIcon name="caret-left" :size="12" />
قبلی
</button>
<ul class="pagination__list">
<li v-for="page in visiblePages" :key="page">
<button
type="button"
class="pagination__page"
:class="{ 'pagination__page--active': page === currentPage }"
@click="changePage(page)"
>
{{ page }}
</button>
</li>
</ul>
<button
type="button"
:disabled="isLastPage"
class="pagination__nav"
:class="{ 'pagination__nav--disabled': isLastPage }"
@click="changePage(currentPage + 1)"
>
بعدی
<SvgIcon name="caret-right" :size="12" />
</button>
</div>
</template>
<script setup>
import { computed } from 'vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
const props = defineProps({
pagination: { type: Object, required: true },
})
const emit = defineEmits(['update:page', 'change'])
const currentPage = computed(() => props.pagination.page || 1)
const totalPages = computed(() => props.pagination.lastPage || props.pagination.last_page || 1)
const isFirstPage = computed(() => currentPage.value === 1)
const isLastPage = computed(() => currentPage.value === totalPages.value)
const visiblePages = computed(() => {
const total = totalPages.value
const current = currentPage.value
const range = 2
let start = Math.max(1, current - range)
let end = Math.min(total, current + range)
if (start > 1) start = start === 2 ? 1 : start
if (end < total) end = end === total - 1 ? total : end
const pages = []
for (let i = start; i <= end; i += 1) pages.push(i)
return pages
})
const changePage = (page) => {
if (page < 1 || page > totalPages.value) return
emit('update:page', page)
emit('change', page)
}
</script>
<style lang="scss" scoped>
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
direction: ltr;
&__nav {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
border: none;
background: #e9e9e9;
border-radius: 0.375rem;
font-family: var(--font-family-fa);
cursor: pointer;
&--disabled {
background: transparent;
color: #d1d5db;
cursor: not-allowed;
}
}
&__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
align-items: center;
gap: 0.625rem;
}
&__page {
height: 1.5rem;
width: 1.5rem;
line-height: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
border-radius: 0.375rem;
font-family: var(--font-family-number-fa);
font-size: 0.875rem;
cursor: pointer;
&--active {
background: #e9e9e9;
}
}
}
</style>
@@ -0,0 +1,32 @@
<template>
<div class="simple-title">
<slot name="header-icon" />
<p class="simple-title__text">{{ title }}</p>
</div>
</template>
<script setup>
defineProps({
title: { type: String, default: '' },
})
</script>
<style lang="scss" scoped>
.simple-title {
background: rgba(255, 255, 255, 80%);
box-shadow: 0 10px 15px -3px rgba(241, 241, 241, 100%);
padding: 0.875rem;
margin-bottom: 0.625rem;
border-radius: 0.75rem;
display: flex;
align-items: center;
gap: 0.375rem;
&__text {
font-family: var(--font-family-fa);
font-weight: 500;
font-size: 0.875rem;
margin: 0;
}
}
</style>
@@ -0,0 +1,56 @@
<template>
<div class="skeleton-loader" :class="customClass">
<div v-for="n in rows" :key="n" class="skeleton-loader__row">
<div
v-for="i in colsPerRow"
:key="i"
class="skeleton-loader__cell"
:style="{ background: bgColor }"
/>
</div>
</div>
</template>
<script setup>
defineProps({
rows: { type: Number, default: 5 },
colsPerRow: { type: Number, default: 2 },
customClass: { type: String, default: '' },
bgColor: { type: String, default: '#f9f9f9' },
})
</script>
<style lang="scss" scoped>
.skeleton-loader {
display: flex;
flex-direction: column;
gap: 0.75rem;
animation: skeleton-pulse 1.5s ease-in-out infinite;
&__row {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
background: #f9f9f9;
border-radius: 0.75rem;
}
&__cell {
flex: 1 1 0;
min-height: 4rem;
border-radius: 0.75rem;
}
}
@keyframes skeleton-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
</style>
+91
View File
@@ -0,0 +1,91 @@
<template>
<div class="tabs-block">
<div class="tabs-block__nav">
<button
v-for="tab in tabs"
:key="tab.name"
type="button"
class="tabs-block__btn"
:class="{ 'tabs-block__btn--active': activeTab === tab.name }"
@click="onChange(tab.name)"
>
<SvgIcon
v-if="tab.icon"
:name="tab.icon"
:size="16"
:color="activeTab === tab.name ? 'var(--color-primary)' : '#bcbcbc'"
/>
<span>{{ tab.label }}</span>
</button>
</div>
<div class="tabs-block__content">
<slot :name="activeTab" />
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
const props = defineProps({
tabs: { type: Array, required: true },
defaultTab: { type: String, default: '' },
modelValue: { type: String, default: '' },
})
const emit = defineEmits(['update:modelValue', 'change-tab'])
const activeTab = ref(props.modelValue || props.defaultTab || props.tabs[0]?.name || '')
watch(
() => props.modelValue,
(val) => {
if (val) activeTab.value = val
}
)
const onChange = (name) => {
activeTab.value = name
emit('update:modelValue', name)
emit('change-tab', name)
}
</script>
<style lang="scss" scoped>
.tabs-block {
&__nav {
display: flex;
gap: 0.5rem;
border-block-end: 1px solid #eee;
margin-bottom: 1rem;
overflow: auto hidden;
}
&__btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.625rem 1rem;
border: none;
background: transparent;
cursor: pointer;
font-family: var(--font-family-fa);
font-size: 0.875rem;
color: #848484;
border-block-end: 2px solid transparent;
margin-block-end: -1px;
white-space: nowrap;
&--active {
color: var(--color-primary);
border-color: var(--color-primary);
}
}
&__content {
padding-block: 0.5rem;
}
}
</style>
@@ -0,0 +1,9 @@
<template>
<SelectField v-bind="$attrs" :searchable="true" />
</template>
<script setup>
import SelectField from '@/components/form/SelectField.vue'
defineOptions({ inheritAttrs: false })
</script>
+257
View File
@@ -0,0 +1,257 @@
<template>
<div class="datepicker-field">
<label v-if="label" :for="inputId" class="datepicker-field__label">{{ label }}</label>
<div
:id="inputId"
ref="referenceEl"
tabindex="0"
class="datepicker-field__trigger"
:class="{
'datepicker-field__trigger--error': error,
'datepicker-field__trigger--disabled': disabled,
}"
@click="toggle"
>
<span class="datepicker-field__value">{{ displayValue }}</span>
<SvgIcon name="calendar" :size="20" class="datepicker-field__icon" />
</div>
<div v-if="error" class="datepicker-field__error" :class="{ 'animate-shake': vibration }">
{{ error }}
</div>
<Teleport to="body">
<Transition name="fade">
<div
v-show="isOpen"
ref="floatingEl"
class="date-picker-dropdown datepicker-field__dropdown"
:style="floatingStyles"
>
<DatePicker
v-model="internalValue"
locale="fa"
inline
editable
:min="min"
:max="max"
:type="type"
:simple="simple"
:auto-submit="type === 'date'"
:format="type === 'datetime' ? 'jYYYY/jMM/jDD HH:mm' : 'jYYYY/jMM/jDD'"
:display-format="type === 'datetime' ? 'jYYYY/jMM/jDD HH:mm' : 'jYYYY/jMM/jDD'"
@update:model-value="onSelect"
/>
</div>
</Transition>
</Teleport>
</div>
</template>
<script setup>
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import DatePicker from 'vue3-persian-datetime-picker'
import { autoUpdate, computePosition, flip, offset, shift, size } from '@floating-ui/dom'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import {
formatJalaaliDate,
formatJalaaliDateTime,
jalaaliStringToIsoDate,
jalaaliStringToIsoDateTime,
} from '@/utils/date-utils'
let uid = 0
const nextUid = () => `datepicker-field-${++uid}`
const props = defineProps({
modelValue: { type: String, default: '' },
name: { type: String, default: '' },
label: { type: String, default: '' },
placeholder: { type: String, default: 'یک تاریخ را انتخاب کنید' },
disabled: { type: Boolean, default: false },
error: { type: String, default: '' },
vibration: { type: Boolean, default: false },
min: { type: String, default: undefined },
max: { type: String, default: undefined },
type: { type: String, default: 'date' },
simple: { type: Boolean, default: false },
})
const emit = defineEmits(['update:modelValue', 'change'])
const inputId = computed(() => props.name || nextUid())
const referenceEl = ref(null)
const floatingEl = ref(null)
const isOpen = ref(false)
const internalValue = ref(null)
const floatingStyles = ref({ position: 'absolute', top: '0px', left: '0px' })
let cleanup = null
const displayValue = computed(() => internalValue.value || props.placeholder)
const syncFromProp = () => {
const v = props.modelValue
if (v && typeof v === 'string') {
internalValue.value =
props.type === 'datetime' ? formatJalaaliDateTime(v) : formatJalaaliDate(v)
} else {
internalValue.value = null
}
}
watch(() => props.modelValue, syncFromProp, { immediate: true })
const onSelect = (jalaliValue) => {
if (!jalaliValue) return
internalValue.value = jalaliValue
const gregorian =
props.type === 'datetime'
? jalaaliStringToIsoDateTime(jalaliValue)
: jalaaliStringToIsoDate(jalaliValue)
emit('update:modelValue', gregorian)
emit('change', gregorian)
if (props.type === 'date') close()
}
const mountFloating = () => {
const reference = referenceEl.value
const floating = floatingEl.value
if (!reference || !floating) return
cleanup = autoUpdate(reference, floating, () => {
computePosition(reference, floating, {
placement: 'bottom-start',
middleware: [
offset(6),
flip(),
shift({ padding: 8 }),
size({
apply({ rects }) {
Object.assign(floating.style, { minWidth: `${rects.reference.width}px` })
},
}),
],
}).then(({ x, y }) => {
floatingStyles.value = { position: 'absolute', left: `${x}px`, top: `${y}px` }
})
})
document.addEventListener('mousedown', handleOutsideClick)
}
const unmountFloating = () => {
cleanup?.()
cleanup = null
document.removeEventListener('mousedown', handleOutsideClick)
}
const handleOutsideClick = (event) => {
const reference = referenceEl.value
const floating = floatingEl.value
if (
reference &&
!reference.contains(event.target) &&
floating &&
!floating.contains(event.target)
) {
close()
}
}
const open = () => {
isOpen.value = true
setTimeout(mountFloating, 0)
}
const close = () => {
isOpen.value = false
unmountFloating()
}
const toggle = () => {
if (props.disabled) return
isOpen.value ? close() : open()
}
onBeforeUnmount(() => unmountFloating())
</script>
<style lang="scss" scoped>
.datepicker-field {
position: relative;
width: 100%;
margin-bottom: 0.75rem;
&__label {
display: flex;
margin-bottom: 0.25rem;
font-family: var(--font-family-fa);
font-weight: 300;
line-height: 1.5rem;
font-size: 0.875rem;
color: var(--color-prim-gray);
pointer-events: none;
}
&__trigger {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 2.35rem;
padding: 0 1.25rem;
border: 1px solid var(--color-thd-gray);
border-radius: 1.5rem;
cursor: pointer;
font-family: var(--font-family-number-fa);
font-size: 0.9rem;
transition: all 0.2s;
outline: none;
&:focus {
box-shadow: 0 0 0 1px var(--color-validate);
}
&--error {
border-color: var(--color-error);
&:focus {
box-shadow: 0 0 0 1px var(--color-error);
}
}
&--disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
&__value {
flex: 1;
text-align: start;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__icon {
color: var(--color-thd-gray);
flex-shrink: 0;
}
&__error {
margin-top: 0.25rem;
font-family: var(--font-family-fa);
font-size: 0.75rem;
color: var(--color-error);
}
&__dropdown {
z-index: 50;
border: 1px solid var(--color-thd-gray);
border-radius: 0.75rem;
background: #fff;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 10%), 0 4px 6px -4px rgba(0, 0, 0, 10%);
}
}
</style>
+256
View File
@@ -0,0 +1,256 @@
<template>
<div class="file-uploader">
<label class="file-uploader__dropzone-wrap">
<div
class="file-uploader__dropzone"
:class="{ 'file-uploader__dropzone--active': isDragging }"
@dragover.prevent
@dragenter="isDragging = true"
@dragleave="isDragging = false"
@drop="handleDrop"
>
<SvgIcon name="upload" :size="32" class="file-uploader__upload-icon" />
<span class="file-uploader__upload-text">{{ uploadText }}</span>
<input
type="file"
class="file-uploader__input"
:accept="accept"
:multiple="multiple"
@change="handleFileSelect"
/>
</div>
</label>
<div v-if="modelValue.length > 0" class="file-uploader__list">
<div v-for="(file, index) in modelValue" :key="file.id ?? index" class="file-uploader__item">
<div class="file-uploader__item-info">
<SvgIcon name="file" :size="24" class="file-uploader__item-icon" />
<div class="file-uploader__item-meta">
<p class="file-uploader__item-name">{{ file.name }}</p>
<p class="file-uploader__item-size">{{ formatFileSize(file.size) }}</p>
</div>
</div>
<div class="file-uploader__item-actions">
<img
v-if="file.url && isImage(file)"
:src="file.url"
class="file-uploader__item-thumb"
alt="preview"
/>
<a v-else-if="file.url" :href="file.url" target="_blank" class="file-uploader__item-link">
دانلود
</a>
<CircleButton
tooltip="حذف"
bg-color="transparent"
size="2rem"
@click="removeFile(index, file)"
>
<template #icon>
<SvgIcon name="close" :size="16" />
</template>
</CircleButton>
</div>
</div>
</div>
<div v-else class="file-uploader__empty">هیچ فایلی آپلود نشده است</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
const props = defineProps({
modelValue: { type: Array, default: () => [] },
accept: { type: String, default: '*' },
multiple: { type: Boolean, default: true },
maxFiles: { type: Number, default: 10 },
maxSize: { type: Number, default: 0 },
})
const emit = defineEmits(['update:modelValue', 'select', 'remove', 'error'])
const isDragging = ref(false)
const uploadText = computed(() =>
props.modelValue.length === 0 ? 'فایل جدید اضافه کنید' : 'فایل دیگری اضافه کنید'
)
const handleFileSelect = (event) => {
const files = [...event.target.files]
emitFiles(files)
event.target.value = ''
}
const handleDrop = (event) => {
event.preventDefault()
isDragging.value = false
const files = [...event.dataTransfer.files]
emitFiles(files)
}
const emitFiles = (files) => {
if (props.modelValue.length + files.length > props.maxFiles) {
emit('error', `حداکثر تعداد فایل مجاز ${props.maxFiles} عدد است.`)
return
}
if (props.maxSize > 0) {
const tooBig = files.find((f) => f.size > props.maxSize)
if (tooBig) {
emit('error', `حجم فایل "${tooBig.name}" بیش از حد مجاز است.`)
return
}
}
emit('select', files)
}
const removeFile = (index, file) => {
const next = [...props.modelValue]
next.splice(index, 1)
emit('update:modelValue', next)
emit('remove', { index, file })
}
const isImage = (file) =>
file.type === 'image' || /\.(jpe?g|png|gif|webp|svg)$/i.test(file.name || '')
const formatFileSize = (bytes) => {
if (!bytes) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
}
</script>
<style lang="scss" scoped>
.file-uploader {
width: 100%;
&__dropzone-wrap {
display: block;
width: 100%;
cursor: pointer;
margin-bottom: 0.75rem;
}
&__dropzone {
border: 2px dashed #d1d5db;
border-radius: 0.5rem;
padding: 1rem;
text-align: center;
transition: border-color 0.2s;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
&:hover,
&--active {
border-color: var(--color-primary);
}
}
&__upload-icon {
color: #9ca3af;
}
&__upload-text {
font-family: var(--font-family-fa);
font-size: 0.875rem;
font-weight: 500;
color: #4b5563;
}
&__input {
display: none;
}
&__list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 0.625rem;
}
&__item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem;
background-color: #eee;
border-radius: 0.5rem;
margin-bottom: 1rem;
gap: 0.75rem;
}
&__item-info {
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 0;
}
&__item-icon {
color: #6b7280;
flex-shrink: 0;
}
&__item-meta {
min-width: 0;
}
&__item-name {
font-family: var(--font-family-fa);
font-size: 0.875rem;
font-weight: 500;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__item-size {
font-family: var(--font-family-en);
font-size: 0.75rem;
color: #6b7280;
margin: 0;
}
&__item-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
&__item-thumb {
width: 4rem;
height: 3rem;
object-fit: cover;
border-radius: 0.25rem;
}
&__item-link {
font-family: var(--font-family-fa);
font-size: 0.875rem;
color: var(--color-primary);
text-decoration: none;
&:hover {
opacity: 0.85;
}
}
&__empty {
text-align: center;
padding: 1rem 0;
font-family: var(--font-family-fa);
color: #6b7280;
font-size: 0.875rem;
}
}
</style>
+357
View File
@@ -0,0 +1,357 @@
<template>
<div>
<div class="image-cropper" :style="{ backgroundColor: bgColor }">
<button
v-if="!isCropping && !previewUrl"
type="button"
class="image-cropper__upload-btn"
@click="triggerUpload"
>
<SvgIcon name="upload" :size="64" />
</button>
<div v-if="isCropping && image" class="image-cropper__crop-stage">
<div class="image-cropper__crop-frame">
<Cropper
:src="image"
:stencil-component="circleStencil"
:stencil-props="{ aspectRatio: 1 }"
image-restriction="stencil"
:auto-zoom="true"
class="image-cropper__cropper"
@change="onCrop"
/>
</div>
<div class="image-cropper__actions">
<CircleButton
tooltip="تایید"
tooltip-position="top"
bg-color="var(--color-primary)"
size="2.5rem"
@click.stop="confirmCrop"
>
<template #icon>
<SvgIcon name="check" :size="20" color="#fff" />
</template>
</CircleButton>
<CircleButton
tooltip="انصراف"
tooltip-position="top"
bg-color="#fff"
size="2.5rem"
@click="cancelCrop"
>
<template #icon>
<SvgIcon name="close" :size="20" color="var(--color-thd-gray)" />
</template>
</CircleButton>
</div>
</div>
<div v-else-if="previewUrl && !isCropping" class="image-cropper__preview-wrap">
<div class="image-cropper__preview-frame">
<img :src="previewUrl" alt="" class="image-cropper__preview-img" />
<div class="image-cropper__preview-mask" />
</div>
<div class="image-cropper__preview-action">
<CircleButton
tooltip="بارگذاری"
tooltip-position="top"
bg-color="#fff"
size="2.5rem"
@click="triggerUpload"
>
<template #icon>
<SvgIcon name="upload" :size="20" color="var(--color-thd-gray)" />
</template>
</CircleButton>
</div>
</div>
<div class="image-cropper__hint">
<SvgIcon name="warning" class="image-cropper__hint-icon" />
<p>{{ helperText }}</p>
</div>
</div>
<input ref="fileInput" type="file" accept="image/*" hidden @change="onFileChange" />
<p v-if="error" class="image-cropper__error" :class="{ 'animate-shake': vibration }">
{{ error }}
</p>
</div>
</template>
<script setup>
import { markRaw, onBeforeUnmount, ref, watch } from 'vue'
import { Cropper, CircleStencil } from 'vue-advanced-cropper'
import 'vue-advanced-cropper/dist/style.css'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
const props = defineProps({
modelValue: { type: [Object, String], default: null },
name: { type: String, default: '' },
vibration: { type: Boolean, default: false },
bgColor: { type: String, default: '#f3f4f6' },
maxSize: { type: Number, default: 250 * 1024 },
helperText: {
type: String,
default: 'سایز تصویر رسمی 3x4 و حجم تصویر بیشتر از 250 کیلو بایت نباشد.',
},
error: { type: String, default: '' },
})
const emit = defineEmits(['crop', 'update:modelValue', 'error'])
const fileInput = ref(null)
const image = ref(null)
const cropResult = ref(null)
const isCropping = ref(false)
const internalPreviewUrl = ref(null)
const originalFileName = ref(null)
const previewUrl = ref(null)
const circleStencil = markRaw(CircleStencil)
const cleanup = () => {
if (internalPreviewUrl.value) {
URL.revokeObjectURL(internalPreviewUrl.value)
internalPreviewUrl.value = null
}
}
watch(
() => props.modelValue,
(val) => {
if (val && typeof val === 'object' && val.url) {
previewUrl.value = val.url
} else if (typeof val === 'string') {
previewUrl.value = val
} else {
previewUrl.value = null
}
},
{ immediate: true }
)
const triggerUpload = () => fileInput.value?.click()
const onFileChange = (event) => {
cleanup()
const file = event.target.files?.[0]
if (!file) return
if (props.maxSize > 0 && file.size > props.maxSize) {
emit('error', 'حجم تصویر نباید بیشتر از ۲۵۰ کیلوبایت باشد.')
event.target.value = null
return
}
originalFileName.value = file.name
internalPreviewUrl.value = URL.createObjectURL(file)
image.value = internalPreviewUrl.value
isCropping.value = true
event.target.value = null
}
const onCrop = ({ canvas }) => {
cropResult.value = canvas
}
const confirmCrop = async () => {
if (!cropResult.value) return
const blob = await new Promise((resolve) => cropResult.value.toBlob(resolve, 'image/png'))
const croppedFile = new File([blob], originalFileName.value || 'avatar.png', {
type: 'image/png',
lastModified: Date.now(),
})
emit('crop', croppedFile)
isCropping.value = false
image.value = null
cropResult.value = null
}
const cancelCrop = () => {
isCropping.value = false
image.value = null
cleanup()
}
onBeforeUnmount(cleanup)
</script>
<style lang="scss" scoped>
.image-cropper {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-around;
border-radius: 1.5rem;
text-align: center;
padding: 0.75rem 0.5rem;
@media (min-width: 1024px) {
min-height: 20rem;
padding: 0.75rem;
}
@media (min-width: 1536px) {
min-height: 23rem;
}
&__upload-btn {
height: 8.15rem;
width: 8.15rem;
border-radius: 9999px;
background: #fff;
border: none;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--color-thd-gray);
@media (min-width: 1536px) {
height: 10.95rem;
width: 10.95rem;
}
}
&__crop-stage {
display: flex;
flex-direction: column;
align-items: center;
}
&__crop-frame {
height: 8.15rem;
width: 8.15rem;
border-radius: 9999px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #d1d5db;
@media (min-width: 1536px) {
height: 10.95rem;
width: 10.95rem;
}
}
&__cropper {
border-radius: 9999px;
}
&__actions {
display: flex;
gap: 0.625rem;
align-items: center;
justify-content: center;
margin-top: 0.75rem;
@media (min-width: 1280px) {
margin-top: 1rem;
}
@media (min-width: 1536px) {
margin-top: 1.25rem;
}
}
&__preview-wrap {
position: relative;
width: fit-content;
margin: 0 auto;
}
&__preview-frame {
position: relative;
border-radius: 9999px;
height: 8.15rem;
width: 8.15rem;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
@media (min-width: 1536px) {
height: 10.95rem;
width: 10.95rem;
}
}
&__preview-img {
height: 100%;
width: 100%;
object-fit: cover;
border-radius: 9999px;
}
&__preview-mask {
position: absolute;
inset: 0;
border-radius: 9999px;
border: 1rem solid #000;
opacity: 0.2;
z-index: 0;
}
&__preview-action {
position: absolute;
left: 0.5rem;
bottom: -0.25rem;
z-index: 10;
}
&__hint {
background: #fff;
border-radius: 1rem;
margin-top: 1.25rem;
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
p {
font-family: var(--font-family-fa);
font-size: 0.6875rem;
line-height: 1.5;
text-align: right;
margin: 0;
@media (min-width: 1024px) {
font-size: 0.75rem;
line-height: 1.75rem;
}
}
}
&__error {
margin-top: 0.25rem;
text-align: right;
font-family: var(--font-family-fa);
font-size: 0.6875rem;
color: var(--color-error);
@media (min-width: 1280px) {
font-size: 0.875rem;
}
}
&__hint-icon {
color: var(--color-prim-gray);
min-height: 1.25rem;
min-width: 1.25rem;
@media (min-width: 1024px) {
min-height: 2rem;
min-width: 2rem;
}
}
}
</style>
+68
View File
@@ -0,0 +1,68 @@
<template>
<TextField
v-model="model"
:name="name"
:label="label"
:placeholder="placeholder"
:error="error"
:vibration="vibration"
:disabled="disabled"
:type="visible ? 'text' : 'password'"
>
<template #appendIcon>
<button
type="button"
class="password-field__toggle"
:aria-label="visible ? 'پنهان کردن رمز' : 'نمایش رمز'"
@click="visible = !visible"
>
<SvgIcon :name="visible ? 'eye-slash' : 'eye'" :size="22" color="var(--color-thd-gray)" />
</button>
</template>
</TextField>
</template>
<script setup>
import { ref, watch } from 'vue'
import TextField from '@/components/form/TextField.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
const props = defineProps({
modelValue: { type: String, default: '' },
name: { type: String, default: 'password' },
label: { type: String, default: 'رمز عبور' },
placeholder: { type: String, default: '' },
error: { type: String, default: '' },
vibration: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
})
const emit = defineEmits(['update:modelValue'])
const visible = ref(false)
const model = ref(props.modelValue)
watch(model, (v) => emit('update:modelValue', v))
watch(
() => props.modelValue,
(v) => {
if (v !== model.value) model.value = v
}
)
</script>
<style lang="scss" scoped>
.password-field__toggle {
border: none;
background: transparent;
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
&:hover {
opacity: 0.7;
}
}
</style>
+377
View File
@@ -0,0 +1,377 @@
<template>
<div class="select-field">
<label v-if="label" :for="inputId" class="select-field__label">{{ label }}</label>
<div
:id="inputId"
ref="referenceEl"
tabindex="0"
class="select-field__trigger"
:class="{
'select-field__trigger--error': error,
'select-field__trigger--disabled': disabled,
'select-field__trigger--open': isOpen,
}"
@click="toggle"
>
<span class="select-field__value">{{ selectedLabel }}</span>
<SvgIcon
name="caret-down"
:size="20"
class="select-field__caret"
:class="{ 'select-field__caret--open': isOpen }"
/>
</div>
<div v-if="error" class="select-field__error" :class="{ 'animate-shake': vibration }">
{{ error }}
</div>
<Teleport to="body">
<Transition name="dropdown">
<div
v-if="isOpen"
ref="floatingEl"
class="select-field__dropdown scrollbar-thin"
:style="{ width: dropdownWidth }"
>
<div v-if="searchable" class="select-field__search">
<input
v-model="searchQuery"
type="text"
placeholder="جستجو..."
class="select-field__search-input"
@input="handleSearch"
/>
</div>
<div v-if="options.length === 0" class="select-field__empty">موردی یافت نشد</div>
<ul v-else class="select-field__list">
<li v-for="(opt, i) in options" :key="i">
<button
type="button"
class="select-field__option"
:class="{ 'select-field__option--active': isSelected(opt) }"
@click="select(opt)"
>
<template v-if="multiple">
<SvgIcon
:name="isSelected(opt) ? 'check-square' : 'square'"
:size="16"
class="select-field__check"
/>
</template>
<span class="select-field__option-text">{{ opt[optionLabel] }}</span>
</button>
</li>
</ul>
</div>
</Transition>
</Teleport>
</div>
</template>
<script setup>
import { computed, onBeforeUnmount, ref } from 'vue'
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'
import SvgIcon from '@/components/icons/SvgIcon.vue'
let uid = 0
const nextUid = () => `select-field-${++uid}`
const props = defineProps({
modelValue: { type: [String, Number, Array, Boolean, Object], default: undefined },
name: { type: String, default: '' },
label: { type: String, default: '' },
placeholder: { type: String, default: 'انتخاب کنید' },
options: { type: Array, required: true },
optionLabel: { type: String, default: 'name' },
optionValue: { type: String, default: 'id' },
multiple: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
error: { type: String, default: '' },
vibration: { type: Boolean, default: false },
searchable: { type: Boolean, default: false },
onSearch: { type: Function, default: null },
searchDebounce: { type: Number, default: 400 },
})
const emit = defineEmits(['update:modelValue', 'change'])
const inputId = computed(() => props.name || nextUid())
const referenceEl = ref(null)
const floatingEl = ref(null)
const isOpen = ref(false)
const dropdownWidth = ref('auto')
const searchQuery = ref('')
let cleanup = null
let debounceTimer = null
const isSelected = (opt) => {
const value = opt[props.optionValue]
return props.multiple
? Array.isArray(props.modelValue) && props.modelValue.includes(value)
: props.modelValue === value
}
const selectedLabel = computed(() => {
const value = props.modelValue
if (value === undefined || value === null || value === '') return props.placeholder
if (props.multiple && Array.isArray(value)) {
const selected = props.options.filter((o) => value.includes(o[props.optionValue]))
if (selected.length === 0) return props.placeholder
if (selected.length <= 2) return selected.map((s) => s[props.optionLabel]).join('، ')
return `${selected.length} مورد انتخاب شده`
}
const found = props.options.find((o) => o[props.optionValue] === value)
return found ? found[props.optionLabel] : props.placeholder
})
const select = (opt) => {
if (props.disabled) return
const value = opt[props.optionValue]
if (props.multiple) {
let current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
if (current.includes(value)) {
current = current.filter((v) => v !== value)
} else {
current.push(value)
}
emit('update:modelValue', current)
emit('change', current)
} else {
emit('update:modelValue', value)
emit('change', value)
close()
}
}
const mountFloating = async () => {
await Promise.resolve()
const reference = referenceEl.value
const floating = floatingEl.value
if (!reference || !floating) return
dropdownWidth.value = `${reference.offsetWidth}px`
cleanup = autoUpdate(reference, floating, () => {
computePosition(reference, floating, {
placement: 'bottom-start',
middleware: [offset(4), flip(), shift({ padding: 8 })],
}).then(({ x, y }) => {
Object.assign(floating.style, { left: `${x}px`, top: `${y}px`, position: 'absolute' })
})
})
document.addEventListener('mousedown', handleOutsideClick)
}
const unmountFloating = () => {
cleanup?.()
cleanup = null
document.removeEventListener('mousedown', handleOutsideClick)
}
const handleOutsideClick = (event) => {
const reference = referenceEl.value
const floating = floatingEl.value
if (
reference &&
!reference.contains(event.target) &&
floating &&
!floating.contains(event.target)
) {
close()
}
}
const open = () => {
isOpen.value = true
setTimeout(mountFloating, 0)
}
const close = () => {
isOpen.value = false
searchQuery.value = ''
unmountFloating()
}
const toggle = () => {
if (props.disabled) return
isOpen.value ? close() : open()
}
const handleSearch = () => {
if (!props.onSearch) return
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => props.onSearch(searchQuery.value), props.searchDebounce)
}
onBeforeUnmount(() => unmountFloating())
</script>
<style lang="scss" scoped>
.select-field {
position: relative;
width: 100%;
margin-bottom: 0.75rem;
&__label {
display: flex;
margin-bottom: 0.25rem;
font-family: var(--font-family-fa);
font-weight: 300;
line-height: 1.5rem;
font-size: 0.875rem;
color: var(--color-prim-gray);
pointer-events: none;
}
&__trigger {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 2.35rem;
padding: 0 1.25rem;
border: 1px solid var(--color-thd-gray);
border-radius: 1.5rem;
background: transparent;
cursor: pointer;
font-family: var(--font-family-number-fa);
font-size: 0.9rem;
transition: all 0.2s;
outline: none;
&:focus {
box-shadow: 0 0 0 1px var(--color-validate);
}
&--error {
border-color: var(--color-error);
&:focus {
box-shadow: 0 0 0 1px var(--color-error);
}
}
&--disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
&__value {
flex: 1;
text-align: start;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__caret {
color: var(--color-thd-gray);
transition: transform 0.2s;
flex-shrink: 0;
&--open {
transform: rotate(180deg);
}
}
&__error {
margin-top: 0.25rem;
font-family: var(--font-family-fa);
font-size: 0.75rem;
color: var(--color-error);
}
&__dropdown {
position: absolute;
z-index: 9999;
max-height: 15rem;
overflow-y: auto;
border: 1px solid var(--color-thd-gray);
border-radius: 1rem;
background: #fff;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 10%), 0 4px 6px -4px rgba(0, 0, 0, 10%);
}
&__search {
padding: 0.75rem;
}
&__search-input {
width: 100%;
padding: 0.5rem 0.75rem;
font-family: var(--font-family-fa);
font-size: 0.875rem;
border: 1px solid var(--color-thd-gray);
border-radius: 0.75rem;
outline: none;
&:focus {
box-shadow: 0 0 0 1px var(--color-validate);
}
}
&__empty {
padding: 0.75rem;
font-family: var(--font-family-fa);
font-size: 0.875rem;
color: var(--color-prim-gray);
}
&__list {
list-style: none;
margin: 0;
padding: 0.25rem;
> li + li {
margin-top: 0.25rem;
}
}
&__option {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.25rem 0.75rem;
border: none;
border-radius: 1.5rem;
background: transparent;
text-align: right;
font-family: var(--font-family-fa);
cursor: pointer;
transition: background-color 0.15s;
&:hover {
background-color: #f3f4f6;
}
&--active {
background-color: #f3f4f6;
}
}
&__check {
color: var(--color-thd-gray);
flex-shrink: 0;
}
&__option-text {
font-size: 0.9rem;
}
}
.dropdown-enter-active,
.dropdown-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-6px);
}
</style>

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