first commit
@@ -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
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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?
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx lint-staged
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
*.min.js
|
||||||
|
package-lock.json
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
*.min.css
|
||||||
@@ -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/**"]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 1024 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 390 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -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';
|
||||||
@@ -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};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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};
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||