Add shadcn-vue combobox components (#30)

This commit is contained in:
Estee Tey 2024-02-13 19:06:09 +08:00 committed by GitHub
parent 063b758124
commit 26a6d1a07e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 4042 additions and 120 deletions

View File

@ -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
View 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"
}
}

View File

@ -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">

View File

@ -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",

View File

@ -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

File diff suppressed because it is too large Load Diff

32
public/favicon.svg Normal file
View 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

View File

@ -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

View File

@ -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

View 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>

View File

@ -0,0 +1 @@
export { default as Combobox } from './Combobox.vue'

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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'

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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'

View 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>

View 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>

View 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>

View 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
View 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
View 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))
}

View File

@ -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')

View File

@ -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
View 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)]
}

View File

@ -12,7 +12,7 @@ export type PresetAttributes = {
name: string
}
type Preset = Required<StyledQRCodeProps> & PresetAttributes
export type Preset = Required<StyledQRCodeProps> & PresetAttributes
const defaultPresetOptions = {
backgroundOptions: {

View File

@ -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]