Add shadcn-vue combobox components (#30)
This commit is contained in:
parent
063b758124
commit
26a6d1a07e
@ -1,19 +1,20 @@
|
||||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution")
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier/skip-formatting',
|
||||
"plugin:tailwindcss/recommended"
|
||||
'plugin:tailwindcss/recommended'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
},
|
||||
rules: {
|
||||
'tailwindcss/no-custom-classname': 'off'
|
||||
}
|
||||
},
|
||||
ignorePatterns: ['**/src/components/ui/**/*.vue']
|
||||
}
|
||||
|
16
components.json
Normal file
16
components.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "default",
|
||||
"typescript": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/style.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true
|
||||
},
|
||||
"framework": "vite",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"Mini QR Code Generator": "Mini QR Code Generator",
|
||||
"Default": "Default",
|
||||
"Randomize style": "Randomize style",
|
||||
"No data!": "No data!",
|
||||
"Change language": "Change language",
|
||||
@ -7,7 +8,7 @@
|
||||
"Data to encode": "Data to encode",
|
||||
"data to encode e.g. a URL or a string": "data to encode e.g. a URL or a string",
|
||||
"Logo image URL": "Logo image URL",
|
||||
"Last saved locally": "Last saved locally",
|
||||
"Last saved locally": "Loaded from local storage",
|
||||
"Loaded from file": "Loaded from file",
|
||||
"Copy QR Code to clipboard": "Copy QR Code to clipboard",
|
||||
"Export as": "Export as",
|
||||
|
@ -23,11 +23,17 @@
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"lucide-vue-next": "^0.321.0",
|
||||
"qr-code-styling": "^1.6.0-rc.1",
|
||||
"radix-vue": "^1.4.1",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"vue": "^3.2.47",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vite": "^5.0.10",
|
||||
"vue": "^3.4.15",
|
||||
"vue-i18n": "9"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
3059
pnpm-lock.yaml
generated
Normal file
3059
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
public/favicon.svg
Normal file
32
public/favicon.svg
Normal file
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="128px" height="128px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g><path style="opacity:1" fill="#f9fafa" d="M -0.5,-0.5 C 5.5,-0.5 11.5,-0.5 17.5,-0.5C 9.16667,3.16667 3.16667,9.16667 -0.5,17.5C -0.5,11.5 -0.5,5.5 -0.5,-0.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#697d80" d="M 17.5,-0.5 C 47.1667,-0.5 76.8333,-0.5 106.5,-0.5C 115.233,1.7243 121.4,7.05764 125,15.5C 125.667,46.8333 125.667,78.1667 125,109.5C 122.245,119.089 116.079,125.089 106.5,127.5C 76.8333,127.5 47.1667,127.5 17.5,127.5C 8.77089,123.77 2.77089,117.437 -0.5,108.5C -0.5,78.1667 -0.5,47.8333 -0.5,17.5C 3.16667,9.16667 9.16667,3.16667 17.5,-0.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#eff1f2" d="M 106.5,-0.5 C 113.5,-0.5 120.5,-0.5 127.5,-0.5C 127.5,42.1667 127.5,84.8333 127.5,127.5C 120.5,127.5 113.5,127.5 106.5,127.5C 116.079,125.089 122.245,119.089 125,109.5C 125.667,78.1667 125.667,46.8333 125,15.5C 121.4,7.05764 115.233,1.7243 106.5,-0.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#a5c4c4" d="M 15.5,12.5 C 23.1739,12.3336 30.8406,12.5003 38.5,13C 39.3333,13.8333 40.1667,14.6667 41,15.5C 41.6667,23.5 41.6667,31.5 41,39.5C 40.5,40 40,40.5 39.5,41C 35.4477,42.1007 31.2811,42.6007 27,42.5C 23.5,42.3333 20,42.1667 16.5,42C 15.5838,41.6258 14.7504,41.1258 14,40.5C 12.396,32.2704 12.0627,23.9371 13,15.5C 14.045,14.6266 14.8783,13.6266 15.5,12.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#a9c9c8" d="M 75.5,12.5 C 76.8333,12.5 78.1667,12.5 79.5,12.5C 79.5,15.5 79.5,18.5 79.5,21.5C 77.8561,21.7135 76.3561,21.3802 75,20.5C 74.2782,17.7395 74.4448,15.0728 75.5,12.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#a5c4c4" d="M 88.5,12.5 C 96.1739,12.3336 103.841,12.5003 111.5,13C 112.333,13.8333 113.167,14.6667 114,15.5C 114.667,23.1667 114.667,30.8333 114,38.5C 113.626,39.4162 113.126,40.2496 112.5,41C 104.939,42.5979 97.2724,42.9313 89.5,42C 88.0692,41.5348 86.9025,40.7014 86,39.5C 85.3333,31.5 85.3333,23.5 86,15.5C 87.045,14.6266 87.8783,13.6266 88.5,12.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#697d80" d="M 18.5,15.5 C 24.1764,15.3339 29.8431,15.5006 35.5,16C 36.3333,16.8333 37.1667,17.6667 38,18.5C 38.6667,24.5 38.6667,30.5 38,36.5C 37.5,37 37,37.5 36.5,38C 29.9184,38.81 23.4184,38.6433 17,37.5C 15.4185,31.2774 15.0852,24.9441 16,18.5C 17.045,17.6266 17.8783,16.6266 18.5,15.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#697d80" d="M 91.5,15.5 C 97.1764,15.3339 102.843,15.5006 108.5,16C 109.333,16.8333 110.167,17.6667 111,18.5C 111.667,24.1667 111.667,29.8333 111,35.5C 110.692,36.3081 110.192,36.9747 109.5,37.5C 103.218,38.626 96.8849,38.7927 90.5,38C 90,37.5 89.5,37 89,36.5C 88.3333,30.5 88.3333,24.5 89,18.5C 90.045,17.6266 90.8783,16.6266 91.5,15.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#8ca7a8" d="M 33.5,34.5 C 33.5,29.8333 33.5,25.1667 33.5,20.5C 29.1667,20.5 24.8333,20.5 20.5,20.5C 20.5,25.1667 20.5,29.8333 20.5,34.5C 19.5128,29.6946 19.1795,24.6946 19.5,19.5C 24.5,19.5 29.5,19.5 34.5,19.5C 34.8205,24.6946 34.4872,29.6946 33.5,34.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#abcbca" d="M 33.5,34.5 C 29.1667,34.5 24.8333,34.5 20.5,34.5C 20.5,29.8333 20.5,25.1667 20.5,20.5C 24.8333,20.5 29.1667,20.5 33.5,20.5C 33.5,25.1667 33.5,29.8333 33.5,34.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#8ca7a8" d="M 106.5,34.5 C 106.5,29.8333 106.5,25.1667 106.5,20.5C 102.167,20.5 97.8333,20.5 93.5,20.5C 93.5,25.1667 93.5,29.8333 93.5,34.5C 92.5128,29.6946 92.1795,24.6946 92.5,19.5C 97.5,19.5 102.5,19.5 107.5,19.5C 107.821,24.6946 107.487,29.6946 106.5,34.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#abcbca" d="M 106.5,34.5 C 102.167,34.5 97.8333,34.5 93.5,34.5C 93.5,29.8333 93.5,25.1667 93.5,20.5C 97.8333,20.5 102.167,20.5 106.5,20.5C 106.5,25.1667 106.5,29.8333 106.5,34.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#a5c5c4" d="M 45.5,24.5 C 57.5046,24.3335 69.5046,24.5001 81.5,25C 82.8333,26.6667 82.8333,28.3333 81.5,30C 69.5,30.6667 57.5,30.6667 45.5,30C 44.2475,28.1722 44.2475,26.3388 45.5,24.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#f4ef99" d="M 62.5,40.5 C 64.1066,42.7177 65.94,44.7177 68,46.5C 70.0577,45.8883 71.891,44.8883 73.5,43.5C 74.48,46.0865 74.8134,48.7531 74.5,51.5C 77.2469,51.1866 79.9135,51.52 82.5,52.5C 81.5,54.5 80.5,56.5 79.5,58.5C 81.4602,60.1563 83.4602,61.823 85.5,63.5C 83.4602,65.177 81.4602,66.8437 79.5,68.5C 80.5,70.5 81.5,72.5 82.5,74.5C 79.9135,75.48 77.2469,75.8134 74.5,75.5C 74.8134,78.2469 74.48,80.9135 73.5,83.5C 68.4793,78.8941 64.9793,79.8941 63,86.5C 61.3903,84.269 59.557,82.269 57.5,80.5C 55.5,81.5 53.5,82.5 51.5,83.5C 50.52,80.9135 50.1866,78.2469 50.5,75.5C 47.7531,75.8134 45.0865,75.48 42.5,74.5C 43.8883,72.891 44.8883,71.0577 45.5,69C 44.2931,66.7341 42.6264,64.9008 40.5,63.5C 42.6264,62.0992 44.2931,60.2659 45.5,58C 44.8883,55.9423 43.8883,54.109 42.5,52.5C 45.4281,51.5159 48.4281,51.1826 51.5,51.5C 51.3359,48.8127 51.5026,46.146 52,43.5C 53.8606,44.7636 55.8606,45.7636 58,46.5C 59.743,44.68 61.243,42.68 62.5,40.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#abcbca" d="M 97.5,44.5 C 99.1667,44.5 100.833,44.5 102.5,44.5C 102.667,57.1711 102.5,69.8377 102,82.5C 100.644,83.3802 99.1439,83.7135 97.5,83.5C 97.5,70.5 97.5,57.5 97.5,44.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#abcbca" d="M 25.5,45.5 C 26.8221,45.33 27.9887,45.6634 29,46.5C 29.4999,58.8288 29.6665,71.1622 29.5,83.5C 27.8561,83.7135 26.3561,83.3802 25,82.5C 24.1882,70.0911 24.3548,57.7577 25.5,45.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#758681" d="M 58.5,49.5 C 66.2689,48.6349 72.1023,51.6349 76,58.5C 77.9382,72.1611 72.1049,78.3277 58.5,77C 50.2985,72.7695 47.4652,66.2695 50,57.5C 52.0738,53.9231 54.9072,51.2565 58.5,49.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#faf39a" d="M 60.5,51.5 C 72.0242,52.5099 76.1909,58.5099 73,69.5C 67.2824,76.5806 60.9491,77.2473 54,71.5C 49.121,62.3222 51.2877,55.6555 60.5,51.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#a6c5c4" d="M 13.5,69.5 C 15.1439,69.2865 16.6439,69.6198 18,70.5C 18.6667,74.5 18.6667,78.5 18,82.5C 16.3333,83.8333 14.6667,83.8333 13,82.5C 12.2316,78.0804 12.3982,73.7471 13.5,69.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#a9c9c8" d="M 88.5,79.5 C 90.1667,79.5 91.8333,79.5 93.5,79.5C 93.6641,82.1873 93.4974,84.854 93,87.5C 91.6667,88.8333 90.3333,88.8333 89,87.5C 88.5026,84.854 88.3359,82.1873 88.5,79.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#a5c4c3" d="M 16.5,86.5 C 23.5079,86.3337 30.5079,86.5004 37.5,87C 38.9308,87.4652 40.0975,88.2986 41,89.5C 41.6667,97.5 41.6667,105.5 41,113.5C 40.6924,114.308 40.1924,114.975 39.5,115.5C 31.5492,116.635 23.5492,116.802 15.5,116C 14.6919,115.692 14.0253,115.192 13.5,114.5C 12.3651,106.549 12.1984,98.5492 13,90.5C 13.6897,88.6498 14.8564,87.3164 16.5,86.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#a6c6c5" d="M 112.5,104.5 C 91.5,104.5 70.5,104.5 49.5,104.5C 49.5,108.167 49.5,111.833 49.5,115.5C 48.1155,116.801 46.6155,116.801 45,115.5C 44.3333,106.5 44.3333,97.5 45,88.5C 46.6667,87.1667 48.3333,87.1667 50,88.5C 50.4983,91.8168 50.665,95.1501 50.5,98.5C 71.5026,98.3334 92.5026,98.5 113.5,99C 114.98,101.235 114.647,103.068 112.5,104.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#a9c9c8" d="M 56.5,91.5 C 65.8333,91.5 75.1667,91.5 84.5,91.5C 84.5,93.1667 84.5,94.8333 84.5,96.5C 75.1667,96.5 65.8333,96.5 56.5,96.5C 56.5,94.8333 56.5,93.1667 56.5,91.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#7d9597" d="M 112.5,104.5 C 92.0067,105.497 71.3401,105.831 50.5,105.5C 50.8128,109.042 50.4794,112.375 49.5,115.5C 49.5,111.833 49.5,108.167 49.5,104.5C 70.5,104.5 91.5,104.5 112.5,104.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#a9c9c8" d="M 87.5,107.5 C 95.84,107.334 104.173,107.5 112.5,108C 115.167,110.667 115.167,113.333 112.5,116C 104.167,116.667 95.8333,116.667 87.5,116C 84.8987,113.17 84.8987,110.337 87.5,107.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#f8f9f9" d="M -0.5,108.5 C 2.77089,117.437 8.77089,123.77 17.5,127.5C 11.5,127.5 5.5,127.5 -0.5,127.5C -0.5,121.167 -0.5,114.833 -0.5,108.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#697d80" d="M 18.5,90.5 C 24.1764,90.3339 29.8431,90.5006 35.5,91C 36.3081,91.3076 36.9747,91.8076 37.5,92.5C 43.3841,110.781 37.0507,117.614 18.5,113C 17.6667,112.167 16.8333,111.333 16,110.5C 15.3333,104.833 15.3333,99.1667 16,93.5C 17.045,92.6266 17.8783,91.6266 18.5,90.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#a8c8c7" d="M 19.5,94.5 C 24.1667,94.5 28.8333,94.5 33.5,94.5C 33.5,99.5 33.5,104.5 33.5,109.5C 28.8333,109.5 24.1667,109.5 19.5,109.5C 19.5,104.5 19.5,99.5 19.5,94.5 Z"/></g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.6 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
207
src/App.vue
207
src/App.vue
@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import StyledQRCode from '@/components/StyledQRCode.vue'
|
||||
import { Combobox } from '@/components/ui/Combobox'
|
||||
import {
|
||||
copyImageToClipboard,
|
||||
downloadPngElement,
|
||||
@ -10,12 +11,24 @@ import type { CornerDotType, CornerSquareType, DotType } from 'qr-code-styling'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import 'vue-i18n'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { createRandomColor, getRandomItemInArray } from './utils/color'
|
||||
import { getNumericCSSValue } from './utils/formatting'
|
||||
import { sortedLocales } from './utils/language'
|
||||
import { allPresets } from './utils/presets'
|
||||
import { allPresets, type Preset } from './utils/presets'
|
||||
|
||||
//#region /** locale */
|
||||
const isLocaleSelectOpen = ref(false)
|
||||
const { t, locale } = useI18n()
|
||||
const locales = computed(() =>
|
||||
sortedLocales.map((loc) => ({
|
||||
value: loc,
|
||||
label: t(loc)
|
||||
}))
|
||||
)
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region /** styling states and computed properties */
|
||||
const defaultPreset = allPresets[0]
|
||||
const data = ref()
|
||||
const image = ref()
|
||||
@ -66,16 +79,6 @@ const qrCodeProps = computed(() => ({
|
||||
imageOptions: imageOptions.value
|
||||
}))
|
||||
|
||||
/* random settings utils */
|
||||
|
||||
function createRandomColor() {
|
||||
return '#' + Math.floor(Math.random() * 16777215).toString(16)
|
||||
}
|
||||
|
||||
function getRandomItemInArray(array: any[]) {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
||||
|
||||
function randomizeStyleSettings() {
|
||||
const dotTypes: DotType[] = [
|
||||
'dots',
|
||||
@ -100,8 +103,14 @@ function randomizeStyleSettings() {
|
||||
styleBackground.value = createRandomColor()
|
||||
}
|
||||
|
||||
const selectedPreset = ref(defaultPreset)
|
||||
|
||||
const isPresetSelectOpen = ref(false)
|
||||
const allPresetOptions = computed(() => {
|
||||
const options = lastCustomLoadedPreset.value
|
||||
? [lastCustomLoadedPreset.value, ...allPresets]
|
||||
: allPresets
|
||||
return options.map((preset) => ({ value: preset.name, label: t(preset.name) }))
|
||||
})
|
||||
const selectedPreset = ref<Preset & { key?: string }>(defaultPreset)
|
||||
watch(selectedPreset, () => {
|
||||
data.value = selectedPreset.value.data
|
||||
image.value = selectedPreset.value.image
|
||||
@ -119,7 +128,32 @@ watch(selectedPreset, () => {
|
||||
styleBackground.value = selectedPreset.value.style.background
|
||||
})
|
||||
|
||||
/* export image utils */
|
||||
const LAST_LOADED_LOCALLY_PRESET_KEY = 'Last saved locally'
|
||||
const LOADED_FROM_FILE_PRESET_KEY = 'Loaded from file'
|
||||
const CUSTOM_LOADED_PRESET_KEYS = [LAST_LOADED_LOCALLY_PRESET_KEY, LOADED_FROM_FILE_PRESET_KEY]
|
||||
const selectedPresetKey = ref<string>(LAST_LOADED_LOCALLY_PRESET_KEY)
|
||||
const lastCustomLoadedPreset = ref<Preset>()
|
||||
watch(
|
||||
selectedPresetKey,
|
||||
(newKey, prevKey) => {
|
||||
if (newKey === prevKey || !newKey) return
|
||||
|
||||
if (CUSTOM_LOADED_PRESET_KEYS.includes(newKey) && lastCustomLoadedPreset.value) {
|
||||
selectedPreset.value = lastCustomLoadedPreset.value
|
||||
return
|
||||
}
|
||||
|
||||
const updatedPreset = allPresets.find((preset) => preset.name === newKey)
|
||||
if (updatedPreset) {
|
||||
selectedPreset.value = updatedPreset
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region /* export image utils */
|
||||
const options = computed(() => ({
|
||||
width: width.value,
|
||||
height: height.value
|
||||
@ -170,8 +204,9 @@ function uploadImage() {
|
||||
imageInput.click()
|
||||
}
|
||||
|
||||
/* QR Config Utils */
|
||||
//#endregion
|
||||
|
||||
//#region /* QR Config Utils */
|
||||
function createQrConfig() {
|
||||
return {
|
||||
props: qrCodeProps.value,
|
||||
@ -197,7 +232,7 @@ function saveQRConfigToLocalStorage() {
|
||||
localStorage.setItem('qrCodeConfig', qrCodeConfigString)
|
||||
}
|
||||
|
||||
function loadQRConfig(jsonString: string, name?: string) {
|
||||
function loadQRConfig(jsonString: string, key?: string) {
|
||||
const qrCodeConfig = JSON.parse(jsonString)
|
||||
const qrCodeProps = qrCodeConfig.props
|
||||
const qrCodeStyle = qrCodeConfig.style
|
||||
@ -206,20 +241,12 @@ function loadQRConfig(jsonString: string, name?: string) {
|
||||
style: qrCodeStyle
|
||||
}
|
||||
|
||||
selectedPreset.value = {
|
||||
...preset,
|
||||
name: name ?? qrCodeProps.name
|
||||
if (key) {
|
||||
preset.name = key
|
||||
lastCustomLoadedPreset.value = preset
|
||||
}
|
||||
}
|
||||
|
||||
function loadQRConfigFromLocalStorage() {
|
||||
const qrCodeConfigString = localStorage.getItem('qrCodeConfig')
|
||||
if (qrCodeConfigString) {
|
||||
console.debug('Loading QR code config from local storage')
|
||||
loadQRConfig(qrCodeConfigString, t('Last saved locally'))
|
||||
} else {
|
||||
selectedPreset.value = { ...defaultPreset }
|
||||
}
|
||||
selectedPreset.value = preset
|
||||
}
|
||||
|
||||
function loadQrConfigFromFile() {
|
||||
@ -235,7 +262,7 @@ function loadQrConfigFromFile() {
|
||||
reader.onload = (event: ProgressEvent<FileReader>) => {
|
||||
const target = event.target as FileReader
|
||||
const result = target.result as string
|
||||
loadQRConfig(result, t('Loaded from file'))
|
||||
loadQRConfig(result, LOADED_FROM_FILE_PRESET_KEY)
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
@ -243,12 +270,15 @@ function loadQrConfigFromFile() {
|
||||
qrCodeConfigInput.click()
|
||||
}
|
||||
|
||||
watch(locale, () => {
|
||||
selectedPreset.value = {
|
||||
...selectedPreset.value,
|
||||
name: t(selectedPreset.value.name)
|
||||
function loadQRConfigFromLocalStorage() {
|
||||
const qrCodeConfigString = localStorage.getItem('qrCodeConfig')
|
||||
if (qrCodeConfigString) {
|
||||
console.debug('Loading QR code config from local storage')
|
||||
loadQRConfig(qrCodeConfigString, LAST_LOADED_LOCALLY_PRESET_KEY)
|
||||
} else {
|
||||
selectedPreset.value = { ...defaultPreset }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(qrCodeProps, () => {
|
||||
saveQRConfigToLocalStorage()
|
||||
@ -257,55 +287,47 @@ watch(qrCodeProps, () => {
|
||||
onMounted(() => {
|
||||
loadQRConfigFromLocalStorage()
|
||||
})
|
||||
//#endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="relative grid place-items-center px-6 py-20 sm:p-8" role="main">
|
||||
<div class="absolute end-4 top-4 flex flex-row items-center gap-4">
|
||||
<div class="flex flex-row items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24">
|
||||
<g
|
||||
fill="none"
|
||||
stroke="#abcbca"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M4 5h7M7 4c0 4.846 0 7 .5 8" />
|
||||
<path
|
||||
d="M10 8.5c0 2.286-2 4.5-3.5 4.5S4 11.865 4 11c0-2 1-3 3-3s5 .57 5 2.857c0 1.524-.667 2.571-2 3.143m2 6l4-9l4 9m-.9-2h-6.2"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<select
|
||||
class="secondary-button cursor-pointer text-center"
|
||||
id="locale-select"
|
||||
v-model="$i18n.locale"
|
||||
:aria-label="t('Change language')"
|
||||
<div class="flex w-full flex-row justify-between p-4 md:w-5/6">
|
||||
<h1>MiniQR</h1>
|
||||
<div class="flex flex-row items-center justify-end gap-4">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24">
|
||||
<g
|
||||
fill="none"
|
||||
stroke="#abcbca"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M4 5h7M7 4c0 4.846 0 7 .5 8" />
|
||||
<path
|
||||
d="M10 8.5c0 2.286-2 4.5-3.5 4.5S4 11.865 4 11c0-2 1-3 3-3s5 .57 5 2.857c0 1.524-.667 2.571-2 3.143m2 6l4-9l4 9m-.9-2h-6.2"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<Combobox :items="locales" v-model:value="locale" v-model:open="isLocaleSelectOpen" />
|
||||
</div>
|
||||
<div class="vertical-border"></div>
|
||||
<a
|
||||
class="icon-button"
|
||||
href="https://github.com/lyqht/styled-qr-code-generator"
|
||||
:aria-label="t('GitHub repository for this project')"
|
||||
>
|
||||
<option v-for="(locale, index) in sortedLocales" :key="index" :value="locale">
|
||||
{{ t(locale) }}
|
||||
</option>
|
||||
</select>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="#abcbca"
|
||||
d="M12.001 2c-5.525 0-10 4.475-10 10a9.994 9.994 0 0 0 6.837 9.488c.5.087.688-.213.688-.476c0-.237-.013-1.024-.013-1.862c-2.512.463-3.162-.612-3.362-1.175c-.113-.288-.6-1.175-1.025-1.413c-.35-.187-.85-.65-.013-.662c.788-.013 1.35.725 1.538 1.025c.9 1.512 2.337 1.087 2.912.825c.088-.65.35-1.087.638-1.337c-2.225-.25-4.55-1.113-4.55-4.938c0-1.088.387-1.987 1.025-2.688c-.1-.25-.45-1.275.1-2.65c0 0 .837-.262 2.75 1.026a9.28 9.28 0 0 1 2.5-.338c.85 0 1.7.112 2.5.337c1.913-1.3 2.75-1.024 2.75-1.024c.55 1.375.2 2.4.1 2.65c.637.7 1.025 1.587 1.025 2.687c0 3.838-2.337 4.688-4.563 4.938c.363.312.676.912.676 1.85c0 1.337-.013 2.412-.013 2.75c0 .262.188.574.688.474A10.016 10.016 0 0 0 22 12c0-5.525-4.475-10-10-10Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="vertical-border"></div>
|
||||
<a
|
||||
class="icon-button"
|
||||
href="https://github.com/lyqht/styled-qr-code-generator"
|
||||
:aria-label="t('GitHub repository for this project')"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="#abcbca"
|
||||
d="M12.001 2c-5.525 0-10 4.475-10 10a9.994 9.994 0 0 0 6.837 9.488c.5.087.688-.213.688-.476c0-.237-.013-1.024-.013-1.862c-2.512.463-3.162-.612-3.362-1.175c-.113-.288-.6-1.175-1.025-1.413c-.35-.187-.85-.65-.013-.662c.788-.013 1.35.725 1.538 1.025c.9 1.512 2.337 1.087 2.912.825c.088-.65.35-1.087.638-1.337c-2.225-.25-4.55-1.113-4.55-4.938c0-1.088.387-1.987 1.025-2.688c-.1-.25-.45-1.275.1-2.65c0 0 .837-.262 2.75 1.026a9.28 9.28 0 0 1 2.5-.338c.85 0 1.7.112 2.5.337c1.913-1.3 2.75-1.024 2.75-1.024c.55 1.375.2 2.4.1 2.65c.637.7 1.025 1.587 1.025 2.687c0 3.838-2.337 4.688-4.563 4.938c.363.312.676.912.676 1.85c0 1.337-.013 2.412-.013 2.75c0 .262.188.574.688.474A10.016 10.016 0 0 0 22 12c0-5.525-4.475-10-10-10Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="w-full md:w-5/6">
|
||||
<div class="mb-8 flex w-full flex-col items-center justify-center">
|
||||
<h1 class="text-4xl">{{ t('Mini QR Code Generator') }}</h1>
|
||||
</div>
|
||||
<div class="flex flex-col-reverse items-start justify-center gap-4 md:flex-row md:gap-12">
|
||||
<div
|
||||
id="main-content"
|
||||
@ -336,7 +358,7 @@ onMounted(() => {
|
||||
<button
|
||||
v-if="IS_COPY_IMAGE_TO_CLIPBOARD_SUPPORTED"
|
||||
id="copy-qr-image-button"
|
||||
class="button flex w-fit flex-row gap-1"
|
||||
class="button flex w-fit max-w-[200px] flex-row items-center gap-1"
|
||||
@click="copyQRToClipboard"
|
||||
:aria-label="t('Copy QR Code to clipboard')"
|
||||
>
|
||||
@ -358,7 +380,7 @@ onMounted(() => {
|
||||
</button>
|
||||
<button
|
||||
id="save-qr-code-config-button"
|
||||
class="button flex w-fit flex-row gap-1"
|
||||
class="button flex w-fit max-w-[200px] flex-row items-center gap-1"
|
||||
@click="downloadQRConfig"
|
||||
:aria-label="t('Save QR Code configuration')"
|
||||
>
|
||||
@ -381,7 +403,7 @@ onMounted(() => {
|
||||
</button>
|
||||
<button
|
||||
id="load-qr-code-config-button"
|
||||
class="button flex w-fit flex-row gap-1"
|
||||
class="button flex w-fit max-w-[200px] flex-row items-center gap-1"
|
||||
@click="loadQrConfigFromFile"
|
||||
:aria-label="t('Load QR Code configuration')"
|
||||
>
|
||||
@ -405,7 +427,7 @@ onMounted(() => {
|
||||
</div>
|
||||
<div id="export-options" class="pt-4">
|
||||
<p class="pb-2">{{ t('Export as') }}</p>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<div class="flex flex-row items-center justify-center gap-2">
|
||||
<button
|
||||
id="download-qr-image-button-png"
|
||||
class="button"
|
||||
@ -465,25 +487,12 @@ onMounted(() => {
|
||||
<div id="settings" class="flex w-full grow flex-col items-start gap-8 text-start">
|
||||
<div>
|
||||
<label for="preset-selector">{{ t('Preset') }}</label>
|
||||
<div class="flex flex-row items-center justify-center gap-2">
|
||||
<select
|
||||
id="preset-selector"
|
||||
class="secondary-button cursor-pointer text-start"
|
||||
:aria-label="t('QR code preset')"
|
||||
v-model="selectedPreset"
|
||||
>
|
||||
<option
|
||||
v-if="!(selectedPreset.name && selectedPreset.name === defaultPreset.name)"
|
||||
:key="`custom-preset`"
|
||||
:value="selectedPreset"
|
||||
disabled
|
||||
>
|
||||
{{ selectedPreset.name }}
|
||||
</option>
|
||||
<option v-for="(preset, index) in allPresets" :key="index" :value="preset">
|
||||
{{ preset.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="flex flex-row items-center justify-start gap-2">
|
||||
<Combobox
|
||||
:items="allPresetOptions"
|
||||
v-model:value="selectedPresetKey"
|
||||
v-model:open="isPresetSelectOpen"
|
||||
/>
|
||||
<button
|
||||
class="icon-button"
|
||||
@click="randomizeStyleSettings"
|
||||
@ -537,7 +546,7 @@ onMounted(() => {
|
||||
<path d="M9.5 13.5L12 11l2.5 2.5" />
|
||||
</g>
|
||||
</svg>
|
||||
<p>{{ t('Upload image') }}</p>
|
||||
<span>{{ t('Upload image') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
|
61
src/components/ui/Combobox/Combobox.vue
Normal file
61
src/components/ui/Combobox/Combobox.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { Check, ChevronsUpDown } from 'lucide-vue-next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList
|
||||
} from '@/components/ui/command'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const open = defineModel<boolean>('open')
|
||||
const value = defineModel<string>('value')
|
||||
|
||||
defineProps<{
|
||||
items: { value: any; label: string }[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover v-model:open="open">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" role="combobox" :aria-expanded="open" class="w-fit justify-between focus-visible:ring-1">
|
||||
{{ value ? items.find((item) => item.value === value)?.label : 'Select item...' }}
|
||||
<ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-fit p-0">
|
||||
<Command>
|
||||
<CommandInput class="h-9" placeholder="Search item..." />
|
||||
<CommandEmpty>No item found.</CommandEmpty>
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
v-for="item in items"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
@select="
|
||||
(ev) => {
|
||||
if (typeof ev.detail.value === 'string') {
|
||||
value = ev.detail.value
|
||||
}
|
||||
open = false
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ item.label }}
|
||||
<Check
|
||||
:class="cn('ml-auto h-4 w-4', value === item.value ? 'opacity-100' : 'opacity-0')"
|
||||
/>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</template>
|
1
src/components/ui/Combobox/index.ts
Normal file
1
src/components/ui/Combobox/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as Combobox } from './Combobox.vue'
|
27
src/components/ui/button/Button.vue
Normal file
27
src/components/ui/button/Button.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { Primitive, type PrimitiveProps } from 'radix-vue'
|
||||
import { type ButtonVariants, buttonVariants } from '.'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
variant?: ButtonVariants['variant']
|
||||
size?: ButtonVariants['size']
|
||||
as?: string
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: 'button'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn(buttonVariants({ variant, size }), props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
31
src/components/ui/button/index.ts
Normal file
31
src/components/ui/button/index.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Button } from './Button.vue'
|
||||
|
||||
export const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'size-10'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>
|
35
src/components/ui/command/Command.vue
Normal file
35
src/components/ui/command/Command.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxRootEmits, ComboboxRootProps } from 'radix-vue'
|
||||
import { ComboboxRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(defineProps<ComboboxRootProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
open: true,
|
||||
modelValue: ''
|
||||
})
|
||||
|
||||
const emits = defineEmits<ComboboxRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxRoot
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</ComboboxRoot>
|
||||
</template>
|
23
src/components/ui/command/CommandDialog.vue
Normal file
23
src/components/ui/command/CommandDialog.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { useForwardPropsEmits } from 'radix-vue'
|
||||
import type { DialogRootEmits, DialogRootProps } from 'radix-vue'
|
||||
import Command from './Command.vue'
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||
|
||||
const props = defineProps<DialogRootProps>()
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-bind="forwarded">
|
||||
<DialogContent class="overflow-hidden p-0 shadow-lg">
|
||||
<Command
|
||||
class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
|
||||
>
|
||||
<slot />
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
20
src/components/ui/command/CommandEmpty.vue
Normal file
20
src/components/ui/command/CommandEmpty.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxEmptyProps } from 'radix-vue'
|
||||
import { ComboboxEmpty } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<ComboboxEmptyProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxEmpty v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)">
|
||||
<slot />
|
||||
</ComboboxEmpty>
|
||||
</template>
|
36
src/components/ui/command/CommandGroup.vue
Normal file
36
src/components/ui/command/CommandGroup.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxGroupProps } from 'radix-vue'
|
||||
import { ComboboxGroup, ComboboxLabel } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<
|
||||
ComboboxGroupProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
heading?: string
|
||||
}
|
||||
>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxGroup
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<ComboboxLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
{{ heading }}
|
||||
</ComboboxLabel>
|
||||
<slot />
|
||||
</ComboboxGroup>
|
||||
</template>
|
40
src/components/ui/command/CommandInput.vue
Normal file
40
src/components/ui/command/CommandInput.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { Search } from 'lucide-vue-next'
|
||||
import { ComboboxInput, type ComboboxInputProps, useForwardProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const props = defineProps<
|
||||
ComboboxInputProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center border-b px-3" cmdk-input-wrapper>
|
||||
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<ComboboxInput
|
||||
v-bind="{ ...forwardedProps, ...$attrs }"
|
||||
auto-focus
|
||||
:class="
|
||||
cn(
|
||||
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
26
src/components/ui/command/CommandItem.vue
Normal file
26
src/components/ui/command/CommandItem.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxItemEmits, ComboboxItemProps } from 'radix-vue'
|
||||
import { ComboboxItem, useForwardPropsEmits } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<ComboboxItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<ComboboxItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxItem
|
||||
v-bind="forwarded"
|
||||
:class="cn('relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</ComboboxItem>
|
||||
</template>
|
29
src/components/ui/command/CommandList.vue
Normal file
29
src/components/ui/command/CommandList.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxContentEmits, ComboboxContentProps } from 'radix-vue'
|
||||
import { ComboboxContent, useForwardPropsEmits } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<ComboboxContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<ComboboxContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxContent
|
||||
v-bind="forwarded"
|
||||
:class="cn('max-h-[300px] overflow-y-auto overflow-x-hidden', props.class)"
|
||||
:dismissable="false"
|
||||
>
|
||||
<div role="presentation">
|
||||
<slot />
|
||||
</div>
|
||||
</ComboboxContent>
|
||||
</template>
|
20
src/components/ui/command/CommandSeparator.vue
Normal file
20
src/components/ui/command/CommandSeparator.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxSeparatorProps } from 'radix-vue'
|
||||
import { ComboboxSeparator } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<ComboboxSeparatorProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxSeparator v-bind="delegatedProps" :class="cn('-mx-1 h-px bg-border', props.class)">
|
||||
<slot />
|
||||
</ComboboxSeparator>
|
||||
</template>
|
14
src/components/ui/command/CommandShortcut.vue
Normal file
14
src/components/ui/command/CommandShortcut.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="cn('ml-auto text-xs tracking-widest text-muted-foreground', props.class)">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
9
src/components/ui/command/index.ts
Normal file
9
src/components/ui/command/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export { default as Command } from './Command.vue'
|
||||
export { default as CommandDialog } from './CommandDialog.vue'
|
||||
export { default as CommandEmpty } from './CommandEmpty.vue'
|
||||
export { default as CommandGroup } from './CommandGroup.vue'
|
||||
export { default as CommandInput } from './CommandInput.vue'
|
||||
export { default as CommandItem } from './CommandItem.vue'
|
||||
export { default as CommandList } from './CommandList.vue'
|
||||
export { default as CommandSeparator } from './CommandSeparator.vue'
|
||||
export { default as CommandShortcut } from './CommandShortcut.vue'
|
19
src/components/ui/dialog/Dialog.vue
Normal file
19
src/components/ui/dialog/Dialog.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DialogRoot,
|
||||
type DialogRootEmits,
|
||||
type DialogRootProps,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
|
||||
const props = defineProps<DialogRootProps>()
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</DialogRoot>
|
||||
</template>
|
11
src/components/ui/dialog/DialogClose.vue
Normal file
11
src/components/ui/dialog/DialogClose.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogClose, type DialogCloseProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<DialogCloseProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogClose v-bind="props">
|
||||
<slot />
|
||||
</DialogClose>
|
||||
</template>
|
51
src/components/ui/dialog/DialogContent.vue
Normal file
51
src/components/ui/dialog/DialogContent.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
type DialogContentEmits,
|
||||
type DialogContentProps,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||
/>
|
||||
<DialogContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</template>
|
24
src/components/ui/dialog/DialogDescription.vue
Normal file
24
src/components/ui/dialog/DialogDescription.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { DialogDescription, type DialogDescriptionProps, useForwardProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogDescription
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('text-sm text-muted-foreground', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DialogDescription>
|
||||
</template>
|
12
src/components/ui/dialog/DialogFooter.vue
Normal file
12
src/components/ui/dialog/DialogFooter.vue
Normal file
@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
14
src/components/ui/dialog/DialogHeader.vue
Normal file
14
src/components/ui/dialog/DialogHeader.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex flex-col gap-y-1.5 text-center sm:text-left', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
24
src/components/ui/dialog/DialogTitle.vue
Normal file
24
src/components/ui/dialog/DialogTitle.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { DialogTitle, type DialogTitleProps, useForwardProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTitle
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('text-lg font-semibold leading-none tracking-tight', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DialogTitle>
|
||||
</template>
|
11
src/components/ui/dialog/DialogTrigger.vue
Normal file
11
src/components/ui/dialog/DialogTrigger.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogTrigger, type DialogTriggerProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<DialogTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTrigger v-bind="props">
|
||||
<slot />
|
||||
</DialogTrigger>
|
||||
</template>
|
8
src/components/ui/dialog/index.ts
Normal file
8
src/components/ui/dialog/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export { default as Dialog } from './Dialog.vue'
|
||||
export { default as DialogClose } from './DialogClose.vue'
|
||||
export { default as DialogTrigger } from './DialogTrigger.vue'
|
||||
export { default as DialogHeader } from './DialogHeader.vue'
|
||||
export { default as DialogTitle } from './DialogTitle.vue'
|
||||
export { default as DialogDescription } from './DialogDescription.vue'
|
||||
export { default as DialogContent } from './DialogContent.vue'
|
||||
export { default as DialogFooter } from './DialogFooter.vue'
|
15
src/components/ui/popover/Popover.vue
Normal file
15
src/components/ui/popover/Popover.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { PopoverRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import type { PopoverRootEmits, PopoverRootProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<PopoverRootProps>()
|
||||
const emits = defineEmits<PopoverRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</PopoverRoot>
|
||||
</template>
|
48
src/components/ui/popover/PopoverContent.vue
Normal file
48
src/components/ui/popover/PopoverContent.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import {
|
||||
PopoverContent,
|
||||
type PopoverContentEmits,
|
||||
type PopoverContentProps,
|
||||
PopoverPortal,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<PopoverContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
align: 'center',
|
||||
sideOffset: 4
|
||||
}
|
||||
)
|
||||
const emits = defineEmits<PopoverContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverPortal>
|
||||
<PopoverContent
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
:class="
|
||||
cn(
|
||||
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</template>
|
11
src/components/ui/popover/PopoverTrigger.vue
Normal file
11
src/components/ui/popover/PopoverTrigger.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { PopoverTrigger, type PopoverTriggerProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<PopoverTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverTrigger v-bind="props">
|
||||
<slot />
|
||||
</PopoverTrigger>
|
||||
</template>
|
3
src/components/ui/popover/index.ts
Normal file
3
src/components/ui/popover/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as Popover } from './Popover.vue'
|
||||
export { default as PopoverTrigger } from './PopoverTrigger.vue'
|
||||
export { default as PopoverContent } from './PopoverContent.vue'
|
78
src/index.css
Normal file
78
src/index.css
Normal file
@ -0,0 +1,78 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import { i18n } from './utils/i18n'
|
||||
import App from './App.vue'
|
||||
import './index.css'
|
||||
import './style.css'
|
||||
|
||||
createApp(App).use(i18n).mount('#app')
|
||||
|
@ -44,6 +44,7 @@ body {
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
#app {
|
||||
@ -64,3 +65,11 @@ h1 {
|
||||
color: #747bff;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
@apply text-balance;
|
||||
}
|
||||
|
||||
svg {
|
||||
@apply shrink-0;
|
||||
}
|
7
src/utils/color.ts
Normal file
7
src/utils/color.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export function createRandomColor() {
|
||||
return '#' + Math.floor(Math.random() * 16777215).toString(16)
|
||||
}
|
||||
|
||||
export function getRandomItemInArray(array: any[]) {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
@ -12,7 +12,7 @@ export type PresetAttributes = {
|
||||
name: string
|
||||
}
|
||||
|
||||
type Preset = Required<StyledQRCodeProps> & PresetAttributes
|
||||
export type Preset = Required<StyledQRCodeProps> & PresetAttributes
|
||||
|
||||
const defaultPresetOptions = {
|
||||
backgroundOptions: {
|
||||
|
@ -1,13 +1,88 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
/* eslint-env node */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
import animate from 'tailwindcss-animate'
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export const darkMode = ['class']
|
||||
export const safelist = ['dark']
|
||||
export const content = [
|
||||
'./pages/**/*.{ts,tsx,vue}',
|
||||
'./components/**/*.{ts,tsx,vue}',
|
||||
'./app/**/*.{ts,tsx,vue}',
|
||||
'./src/**/*.{ts,tsx,vue}'
|
||||
]
|
||||
export const theme = {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
screens: {
|
||||
'2xl': '1400px'
|
||||
}
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
xl: 'calc(var(--radius) + 4px)',
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: { height: 0 },
|
||||
to: { height: 'var(--radix-accordion-content-height)' }
|
||||
},
|
||||
'accordion-up': {
|
||||
from: { height: 'var(--radix-accordion-content-height)' },
|
||||
to: { height: 0 }
|
||||
},
|
||||
'collapsible-down': {
|
||||
from: { height: 0 },
|
||||
to: { height: 'var(--radix-collapsible-content-height)' }
|
||||
},
|
||||
'collapsible-up': {
|
||||
from: { height: 'var(--radix-collapsible-content-height)' },
|
||||
to: { height: 0 }
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
'collapsible-down': 'collapsible-down 0.2s ease-in-out',
|
||||
'collapsible-up': 'collapsible-up 0.2s ease-in-out'
|
||||
}
|
||||
}
|
||||
}
|
||||
export const plugins = [animate]
|
||||
|
Loading…
x
Reference in New Issue
Block a user